-
Is robot currently connected?
-
-
Robot is currently: {connected == null ? "checking..." : (connected ? "connected! 🟢" : "not connected... 🔴")}
-
- {connected == null ? "If checking continues, make sure CB is properly loaded with robot at least once." : ""}
-
-
-
- );
-}
diff --git a/src/pages/Home/Home.module.css b/src/pages/Home/Home.module.css
index 9ec90e5..73f7217 100644
--- a/src/pages/Home/Home.module.css
+++ b/src/pages/Home/Home.module.css
@@ -26,4 +26,52 @@ University within the Software Project course.
display: flex;
flex-direction: column;
gap: 1em;
+}
+
+.links {
+ display: flex;
+ flex-direction: row; /* Horizontal layout looks more like a dashboard */
+ gap: 1.5rem;
+ justify-content: center;
+ margin-top: 2rem;
+}
+
+.navCard {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1rem 2rem;
+ min-width: 180px;
+ background-color: #ffffff;
+ color: #333;
+ text-decoration: none;
+ font-weight: 600;
+ border-radius: 12px;
+ border: 1px solid #e0e0e0;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
+ transition: all 0.2s ease-in-out;
+}
+
+/* Hover effects */
+.navCard:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
+ border-color: #ffcd00; /* UU Yellow accent */
+ color: #000;
+}
+
+/* Specific styling for the logo container */
+.logoPepperScaling {
+ display: flex;
+ justify-content: center;
+ transition: transform 0.3s ease;
+}
+
+.logoPepperScaling:hover {
+ transform: scale(1.05);
+}
+
+.logopepper {
+ height: 120px;
+ width: auto;
}
\ No newline at end of file
diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx
index b2ccb5c..07a2517 100644
--- a/src/pages/Home/Home.tsx
+++ b/src/pages/Home/Home.tsx
@@ -16,15 +16,20 @@ import styles from './Home.module.css'
function Home() {
return (
-
)
diff --git a/src/pages/Manuals/Manuals.module.css b/src/pages/Manuals/Manuals.module.css
new file mode 100644
index 0000000..ebaadf7
--- /dev/null
+++ b/src/pages/Manuals/Manuals.module.css
@@ -0,0 +1,81 @@
+/* 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)
+*/
+
+.manualContainer {
+ width: 100%;
+ max-width: 800px;
+ margin: 4rem auto;
+ padding: 2rem;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ text-align: center;
+}
+
+.manualHeader h1 {
+ font-size: 2.2rem;
+ margin-bottom: 0.5rem;
+}
+
+.buttonStack {
+ display: flex;
+ flex-direction: column; /* Stacks the manual sections vertically */
+ gap: 3rem;
+ margin-top: 3rem;
+ align-items: center;
+}
+
+.manualEntry {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.manualEntry h3 {
+ margin: 0;
+ font-size: 1.4rem;
+}
+
+.manualEntry p {
+ color: #666;
+ margin-bottom: 0.5rem;
+}
+
+.downloadBtn {
+ display: inline-block;
+ background-color: #ffffff; /* White background as requested */
+ color: #000;
+ padding: 1rem 2rem;
+ border-radius: 50px;
+ text-decoration: none;
+ font-weight: 700;
+ font-size: 1.1rem;
+ width: 280px; /* Fixed width for uniform appearance */
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
+ transition: transform 0.2s ease, background-color 0.2s ease, color 0.2s ease;
+}
+
+.downloadBtn:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
+ background-color: #247284; /* Teal hover as requested */
+ color: #ffffff; /* Text turns white on teal for better contrast */
+}
+
+.dateBadge {
+ display: inline-block;
+ margin-top: 1rem;
+ padding: 0.4rem 1rem;
+ background-color: #f0f0f0;
+ border-radius: 4px;
+ font-size: 0.85rem;
+ color: #888;
+}
+
+.divider {
+ margin-top: 4rem;
+ border: 0;
+ border-top: 1px solid #eee;
+ width: 60%;
+}
\ No newline at end of file
diff --git a/src/pages/Manuals/Manuals.tsx b/src/pages/Manuals/Manuals.tsx
new file mode 100644
index 0000000..2c88590
--- /dev/null
+++ b/src/pages/Manuals/Manuals.tsx
@@ -0,0 +1,39 @@
+// 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 styles from './Manuals.module.css';
+
+export default function Manuals() {
+ const userManualPath = "/UserManual.pdf";
+ const developerManualPath = "/DeveloperManual.pdf";
+
+ return (
+
+
+ Documentation & Manuals
+
+ Last Updated: January 2026
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/Robot/Robot.tsx b/src/pages/Robot/Robot.tsx
deleted file mode 100644
index 80f4109..0000000
--- a/src/pages/Robot/Robot.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-// 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 { useState, useEffect, useRef } from 'react'
-import { API_BASE_URL } from '../../config/api.ts';
-
-/**
- * Displays a live robot interaction panel with user input, conversation history,
- * and real-time updates from the robot backend via Server-Sent Events (SSE).
- *
- * @returns A React element rendering the interactive robot UI.
- */
-export default function Robot() {
- /** The text message currently entered by the user. */
- const [message, setMessage] = useState('');
-
- /** Whether the robot’s microphone or listening mode is currently active. */
- const [listening, setListening] = useState(false);
- /** The ongoing conversation history as a sequence of user/assistant messages. */
- const [conversation, setConversation] = useState<
- {"role": "user" | "assistant", "content": string}[]>([])
- /** Reference to the scrollable conversation container for auto-scrolling. */
- const conversationRef = useRef
(null);
- /**
- * Index used to force refresh the SSE connection or clear conversation.
- * Incrementing this value triggers a reset of the live data stream.
- */
- const [conversationIndex, setConversationIndex] = useState(0);
-
- /**
- * Sends a message to the robot backend.
- *
- * Makes a POST request to `/message` with the user’s text.
- * The backend may respond with confirmation or error information.
- */
- const sendMessage = async () => {
- try {
- const response = await fetch(`${API_BASE_URL}/message`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ message }),
- });
- const data = await response.json();
- console.log(data);
- } catch (error) {
- console.error("Error sending message: ", error);
- }
- };
-
- /**
- * Establishes a persistent Server-Sent Events (SSE) connection
- * to receive real-time updates from the robot backend.
- *
- * Handles three event types:
- * - `voice_active`: whether the robot is currently listening.
- * - `speech`: recognized user speech input.
- * - `llm_response`: the robot’s language model-generated reply.
- *
- * The connection resets whenever `conversationIndex` changes.
- */
- useEffect(() => {
- const eventSource = new EventSource(`${API_BASE_URL}/sse`);
-
- eventSource.onmessage = (event) => {
- try {
- const data = JSON.parse(event.data);
- if ("voice_active" in data) setListening(data.voice_active);
- if ("speech" in data) setConversation(conversation => [...conversation, {"role": "user", "content": data.speech}]);
- if ("llm_response" in data) setConversation(conversation => [...conversation, {"role": "assistant", "content": data.llm_response}]);
- } catch {
- console.log("Unparsable SSE message:", event.data);
- }
- };
-
- return () => {
- eventSource.close();
- };
- }, [conversationIndex]);
-
- /**
- * Automatically scrolls the conversation view to the bottom
- * whenever a new message is added.
- */
- useEffect(() => {
- if (!conversationRef || !conversationRef.current) return;
- conversationRef.current.scrollTop = conversationRef.current.scrollHeight;
- }, [conversation]);
-
- return (
- <>
- Robot interaction
- Force robot speech
-
- setMessage(e.target.value)}
- onKeyDown={(e) => e.key === "Enter" && sendMessage().then(() => setMessage(""))}
- placeholder="Enter a message"
- />
-
-
-
-
Conversation
-
Listening {listening ? "🟢" : "🔴"}
-
- {conversation.map((item, i) => (
-
{item["content"]}
- ))}
-
-
-
-
-
-
- >
- );
-}
diff --git a/test/pages/connectedRobots/ConnectedRobots.test.tsx b/test/pages/connectedRobots/ConnectedRobots.test.tsx
deleted file mode 100644
index e5b1fc3..0000000
--- a/test/pages/connectedRobots/ConnectedRobots.test.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-// 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 { render, screen, act, cleanup, waitFor } from '@testing-library/react';
-import ConnectedRobots from '../../../src/pages/ConnectedRobots/ConnectedRobots';
-
-// Mock event source
-const mockInstances: MockEventSource[] = [];
-class MockEventSource {
- url: string;
- onmessage: ((event: MessageEvent) => void) | null = null;
- closed = false;
-
- constructor(url: string) {
- this.url = url;
- mockInstances.push(this);
- }
-
- sendMessage(data: string) {
- // Trigger whatever the component listens to
- this.onmessage?.({ data } as MessageEvent);
- }
-
- close() {
- this.closed = true;
- }
-}
-
-// mock event source generation with fake function that returns our fake mock source
-beforeAll(() => {
- // Cast globalThis to a type exposing EventSource and assign a mocked constructor.
- (globalThis as unknown as { EventSource?: typeof EventSource }).EventSource =
- jest.fn((url: string) => new MockEventSource(url)) as unknown as typeof EventSource;
-});
-
-// clean after tests
-afterEach(() => {
- cleanup();
- jest.restoreAllMocks();
- mockInstances.length = 0;
-});
-
-describe('ConnectedRobots', () => {
- test('renders initial state correctly', () => {
- render();
-
- // Check initial texts (before connection)
- expect(screen.getByText('Is robot currently connected?')).toBeInTheDocument();
- expect(screen.getByText(/Robot is currently:\s*checking/i)).toBeInTheDocument();
- expect(
- screen.getByText(/If checking continues, make sure CB is properly loaded/i)
- ).toBeInTheDocument();
- });
-
- test('updates to connected when message data is true', async () => {
- render();
- const eventSource = mockInstances[0];
- expect(eventSource).toBeDefined();
-
- // Check state after getting 'true' message
- await act(async () => {
- eventSource.sendMessage('true');
- });
-
- await waitFor(() => {
- expect(screen.getByText(/connected! 🟢/i)).toBeInTheDocument();
- });
- });
-
- test('updates to not connected when message data is false', async () => {
- render();
- const eventSource = mockInstances[0];
-
- // Check statew after getting 'false' message
- await act(async () => {
- eventSource.sendMessage('false');
- });
-
- await waitFor(() => {
- expect(screen.getByText(/not connected.*🔴/i)).toBeInTheDocument();
- });
- });
-
- test('handles invalid JSON gracefully', async () => {
- const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
- render();
- const eventSource = mockInstances[0];
-
- await act(async () => {
- eventSource.sendMessage('not-json');
- });
-
- expect(logSpy).toHaveBeenCalledWith(
- 'Ping message not in correct format:',
- 'not-json'
- );
- });
-
- test('closes EventSource on unmount', () => {
- render();
- const eventSource = mockInstances[0];
- const closeSpy = jest.spyOn(eventSource, 'close');
- cleanup();
- expect(closeSpy).toHaveBeenCalled();
- expect(eventSource.closed).toBe(true);
- });
-});
\ No newline at end of file
diff --git a/test/pages/robot/Robot.test.tsx b/test/pages/robot/Robot.test.tsx
deleted file mode 100644
index d2bd07a..0000000
--- a/test/pages/robot/Robot.test.tsx
+++ /dev/null
@@ -1,171 +0,0 @@
-// 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 { render, screen, act, cleanup, fireEvent } from '@testing-library/react';
-import Robot from '../../../src/pages/Robot/Robot';
-import { API_BASE_URL } from '../../../src/config/api.ts';
-
-// Mock EventSource
-const mockInstances: MockEventSource[] = [];
-class MockEventSource {
- url: string;
- onmessage: ((event: MessageEvent) => void) | null = null;
- closed = false;
-
- constructor(url: string) {
- this.url = url;
- mockInstances.push(this);
- }
-
- sendMessage(data: string) {
- this.onmessage?.({ data } as MessageEvent);
- }
-
- close() {
- this.closed = true;
- }
-}
-
-// Mock global EventSource
-beforeAll(() => {
- (globalThis as any).EventSource = jest.fn((url: string) => new MockEventSource(url));
-});
-
-// Mock fetch
-beforeEach(() => {
- globalThis.fetch = jest.fn(() =>
- Promise.resolve({
- json: () => Promise.resolve({ reply: 'ok' }),
- })
- ) as jest.Mock;
-});
-
-// Cleanup
-afterEach(() => {
- cleanup();
- jest.restoreAllMocks();
- mockInstances.length = 0;
-});
-
-describe('Robot', () => {
- test('renders initial state', () => {
- render();
- expect(screen.getByText('Robot interaction')).toBeInTheDocument();
- expect(screen.getByText('Force robot speech')).toBeInTheDocument();
- expect(screen.getByText('Listening 🔴')).toBeInTheDocument();
- expect(screen.getByPlaceholderText('Enter a message')).toBeInTheDocument();
- });
-
- test('sends message via button', async () => {
- render();
- const input = screen.getByPlaceholderText('Enter a message');
- const button = screen.getByText('Speak');
-
- fireEvent.change(input, { target: { value: 'Hello' } });
- await act(async () => fireEvent.click(button));
-
- expect(globalThis.fetch).toHaveBeenCalledWith(
- `${API_BASE_URL}/message`,
- expect.objectContaining({
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message: 'Hello' }),
- })
- );
- });
-
- test('sends message via Enter key', async () => {
- render();
- const input = screen.getByPlaceholderText('Enter a message');
- fireEvent.change(input, { target: { value: 'Hi Enter' } });
-
- await act(async () =>
- fireEvent.keyDown(input, { key: 'Enter', code: 'Enter', charCode: 13 })
- );
-
- expect(globalThis.fetch).toHaveBeenCalledWith(
- `${API_BASE_URL}/message`,
- expect.objectContaining({
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message: 'Hi Enter' }),
- })
- );
- expect((input as HTMLInputElement).value).toBe('');
- });
-
- test('handles fetch errors', async () => {
- globalThis.fetch = jest.fn(() => Promise.reject('Network error')) as jest.Mock;
- const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
-
- render();
- const input = screen.getByPlaceholderText('Enter a message');
- const button = screen.getByText('Speak');
- fireEvent.change(input, { target: { value: 'Error test' } });
-
- await act(async () => fireEvent.click(button));
-
- expect(consoleSpy).toHaveBeenCalledWith(
- 'Error sending message: ',
- 'Network error'
- );
- });
-
- test('updates conversation on SSE', async () => {
- render();
- const eventSource = mockInstances[0];
-
- await act(async () => {
- eventSource.sendMessage(JSON.stringify({ voice_active: true }));
- eventSource.sendMessage(JSON.stringify({ speech: 'User says hi' }));
- eventSource.sendMessage(JSON.stringify({ llm_response: 'Assistant replies' }));
- });
-
- expect(screen.getByText('Listening 🟢')).toBeInTheDocument();
- expect(screen.getByText('User says hi')).toBeInTheDocument();
- expect(screen.getByText('Assistant replies')).toBeInTheDocument();
- });
-
- test('handles invalid SSE JSON', async () => {
- const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
- render();
- const eventSource = mockInstances[0];
-
- await act(async () => eventSource.sendMessage('bad-json'));
-
- expect(logSpy).toHaveBeenCalledWith('Unparsable SSE message:', 'bad-json');
- });
-
- test('resets conversation with Reset button', async () => {
- render();
- const eventSource = mockInstances[0];
-
- await act(async () =>
- eventSource.sendMessage(JSON.stringify({ speech: 'Hello' }))
- );
- expect(screen.getByText('Hello')).toBeInTheDocument();
-
- fireEvent.click(screen.getByText('Reset'));
- expect(screen.queryByText('Hello')).not.toBeInTheDocument();
- });
-
- test('toggles conversationIndex with Stop/Start button', () => {
- render();
- const stopButton = screen.getByText('Stop');
- fireEvent.click(stopButton);
- expect(screen.getByText('Start')).toBeInTheDocument();
-
- fireEvent.click(screen.getByText('Start'));
- expect(screen.getByText('Stop')).toBeInTheDocument();
- });
-
- test('closes EventSource on unmount', () => {
- const { unmount } = render();
- const eventSource = mockInstances[0];
- const closeSpy = jest.spyOn(eventSource, 'close');
-
- unmount();
- expect(closeSpy).toHaveBeenCalled();
- expect(eventSource.closed).toBe(true);
- });
-});