diff --git a/src/App.tsx b/src/App.tsx index c9ac2d7..e2fd7b6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,8 +4,7 @@ import { Routes, Route, Link } from 'react-router' import './App.css' import Home from './pages/Home/Home.tsx' -import Robot from './pages/Robot/Robot.tsx'; -import ConnectedRobots from './pages/ConnectedRobots/ConnectedRobots.tsx' +import UserManual from './pages/Manuals/Manuals.tsx'; import VisProg from "./pages/VisProgPage/VisProg.tsx"; import {useState} from "react"; import Logging from "./components/Logging/Logging.tsx"; @@ -26,8 +25,7 @@ function App(){ } /> } /> - } /> - } /> + } /> {showLogs && } diff --git a/src/pages/ConnectedRobots/ConnectedRobots.tsx b/src/pages/ConnectedRobots/ConnectedRobots.tsx deleted file mode 100644 index 0f97e27..0000000 --- a/src/pages/ConnectedRobots/ConnectedRobots.tsx +++ /dev/null @@ -1,64 +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 { useEffect, useState } from 'react' -import { API_BASE_URL } from '../../config/api.ts'; - -/** - * Displays the current connection status of a robot in real time. - * - * Opens an SSE connection to the backend (`/robot/ping_stream`) that emits - * simple boolean JSON messages (`true` or `false`). Updates automatically when - * the robot connects or disconnects. - * - * @returns A React element showing the current robot connection status. - */ -export default function ConnectedRobots() { - - /** - * 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(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` - 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 ( -
-

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 (
-
- - Pepper logo +
+ + Pepper logo
+
- Robot Interaction → - Editor → - Connected Robots → + {/* Program Editor is now first */} + + Program Editor + + + User and Developer Manual +
) 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 +
+ +
+
+

User Manual

+

Manual for Users of the Pepper+ Software

+ + Download User Manual + +
+ +
+

Developer Manual

+

Technical documentation for future developers.

+ + Download Developer Manual + +
+
+ +
+
+ ); +} \ 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); - }); -});