From c577daa9e6d750f730b85ade0e4978072f2c4179 Mon Sep 17 00:00:00 2001 From: Kasper Date: Fri, 26 Sep 2025 21:39:11 +0200 Subject: [PATCH 1/6] feat: add basic UI2CB and CB2UI communication By pressing the button, the text in the input field is sent to the CB. Every second, UI receives the current time from CB. ref: N25B-107 ref: N25B-110 --- src/App.tsx | 68 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 3d7ded3..e9ce59b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,35 +1,55 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' +import { useState, useEffect } from 'react' import './App.css' function App() { - const [count, setCount] = useState(0) + const [message, setMessage] = useState(''); + const [sseMessage, setSseMessage] = useState(''); + + const sendMessage = async () => { + try { + const response = await fetch("http://localhost:8000/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); + } + }; + + useEffect(() => { + const eventSource = new EventSource("http://localhost:8000/sse"); + + eventSource.onmessage = (event) => { + setSseMessage(event.data); + }; + + return () => { + eventSource.close(); + }; + }); return ( - <> +
- - Vite logo - - - React logo - + setMessage(e.target.value)} + placeholder="Enter a message" + /> +
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

+
+

Message from Server (SSE):

+

{sseMessage}

-

- Click on the Vite and React logos to learn more -

- - ) +
+ ); } export default App From c512739a25b29f9631db23014f7a0dd40ac33496 Mon Sep 17 00:00:00 2001 From: Twirre Meulenbelt <43213592+TwirreM@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:17:16 +0200 Subject: [PATCH 2/6] feat: differentiate between SSE messages For spoken text, we have JSON data that can be differentiated from other data. We show this spoken text in a different UI field. ref: N25B-110 --- src/App.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index e9ce59b..53505d5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import './App.css' function App() { const [message, setMessage] = useState(''); const [sseMessage, setSseMessage] = useState(''); + const [spoken, setSpoken] = useState(""); const sendMessage = async () => { try { @@ -26,12 +27,17 @@ function App() { eventSource.onmessage = (event) => { setSseMessage(event.data); + + try { + const data = JSON.parse(event.data); + if (data.speech) setSpoken(data.speech); + } catch {} }; return () => { eventSource.close(); }; - }); + }, []); return (
@@ -40,6 +46,7 @@ function App() { type="text" value={message} onChange={(e) => setMessage(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && sendMessage().then(() => setMessage(""))} placeholder="Enter a message" /> @@ -48,6 +55,10 @@ function App() {

Message from Server (SSE):

{sseMessage}

+
+

Spoken text (SSE):

+

{spoken}

+
); } From 96053e798ad07f0eaa6415de75f41bbfee453f09 Mon Sep 17 00:00:00 2001 From: JobvAlewijk Date: Wed, 1 Oct 2025 14:06:30 +0200 Subject: [PATCH 3/6] fix: moved ui2cb communication into server --- src/App.tsx | 68 ++--------------------- src/pages/Home/Home.tsx | 4 +- src/pages/ServerComms/ServerComms.css | 0 src/pages/ServerComms/ServerComms.tsx | 80 +++++++++++++++++++++++++++ src/pages/TemplatePage/Template.tsx | 3 +- 5 files changed, 88 insertions(+), 67 deletions(-) create mode 100644 src/pages/ServerComms/ServerComms.css create mode 100644 src/pages/ServerComms/ServerComms.tsx diff --git a/src/App.tsx b/src/App.tsx index 8c9d79d..757e769 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,78 +1,18 @@ -import { useState, useEffect } from 'react' import { Routes, Route } from 'react-router' import './App.css' import TemplatePage from './pages/TemplatePage/Template.tsx' import Home from './pages/Home/Home.tsx' +import ServerComms from './pages/ServerComms/ServerComms.tsx' -function App() { - const [message, setMessage] = useState(''); - const [sseMessage, setSseMessage] = useState(''); - const [spoken, setSpoken] = useState(""); - - const sendMessage = async () => { - try { - const response = await fetch("http://localhost:8000/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); - } - }; - - useEffect(() => { - const eventSource = new EventSource("http://localhost:8000/sse"); - - eventSource.onmessage = (event) => { - setSseMessage(event.data); - - try { - const data = JSON.parse(event.data); - if (data.speech) setSpoken(data.speech); - } catch {} - }; - - return () => { - eventSource.close(); - }; - }, []); - - return ( -
-
- setMessage(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && sendMessage().then(() => setMessage(""))} - placeholder="Enter a message" - /> - -
-
-

Message from Server (SSE):

-

{sseMessage}

-
-
-

Spoken text (SSE):

-

{spoken}

-
-
- ); -/* - function App(){ +function App(){ return ( } /> } /> + } /> ) } -*/ + export default App diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 5c4a72d..8767db3 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +//import { useState } from 'react' import { Link } from 'react-router' import reactLogo from '../../assets/react.svg' import viteLogo from '../../assets/vite.svg' @@ -29,7 +29,7 @@ function Home() {

Vite + React

- + diff --git a/src/pages/ServerComms/ServerComms.css b/src/pages/ServerComms/ServerComms.css new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/ServerComms/ServerComms.tsx b/src/pages/ServerComms/ServerComms.tsx new file mode 100644 index 0000000..c16ec81 --- /dev/null +++ b/src/pages/ServerComms/ServerComms.tsx @@ -0,0 +1,80 @@ +import { useState, useEffect } from 'react' +import { Link } from 'react-router' +//import Counter from '../../components/components.tsx' + + +//this is your css file where you can style your buttons and such +//you can still use css parts from App.css, but also overwrite them + +function ServerComms() { + const [message, setMessage] = useState(''); + const [sseMessage, setSseMessage] = useState(''); + const [spoken, setSpoken] = useState(""); + + const sendMessage = async () => { + try { + const response = await fetch("http://localhost:8000/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); + } + }; + + useEffect(() => { + const eventSource = new EventSource("http://localhost:8000/sse"); + + eventSource.onmessage = (event) => { + setSseMessage(event.data); + + try { + const data = JSON.parse(event.data); + if (data.speech) setSpoken(data.speech); + } catch {} + }; + + return () => { + eventSource.close(); + }; + }, []); + + return ( +
+
+ setMessage(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && sendMessage().then(() => setMessage(""))} + placeholder="Enter a message" + /> + +
+
+

Message from Server (SSE):

+

{sseMessage}

+
+
+

Spoken text (SSE):

+

{spoken}

+
+
+ {/* here you link to the homepage, in App.tsx you can link new pages */} + + +
+
+ + + ); +} + +export default ServerComms \ No newline at end of file diff --git a/src/pages/TemplatePage/Template.tsx b/src/pages/TemplatePage/Template.tsx index 4cb3118..1a3feac 100644 --- a/src/pages/TemplatePage/Template.tsx +++ b/src/pages/TemplatePage/Template.tsx @@ -1,7 +1,8 @@ import { useState } from 'react' import { Link } from 'react-router' import Counter from '../../components/components.tsx' -import style from './Template.module.css' +//import style from './Template.module.css' +import '../../App.css' //this is your css file where you can style your buttons and such //you can still use css parts from App.css, but also overwrite them From c515f32023f789a8dff40c9e01864204a23d0a5e Mon Sep 17 00:00:00 2001 From: Twirre Meulenbelt <43213592+TwirreM@users.noreply.github.com> Date: Wed, 8 Oct 2025 13:07:47 +0200 Subject: [PATCH 4/6] chore: fix branch naming regex pattern The previous pre-commit script allowed only branch names with three words. Should allow one to 6 words. ref: N25B-89 --- .githooks/pre-commit | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 391d279..ed801d8 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -8,11 +8,11 @@ if echo "$branch" | grep -Eq "(dev|main)"; then fi # allowed pattern -if echo "$branch" | grep -Eq "^(feat|fix|refactor|perf|style|test|docs|build|chore|revert)\/\w+-\w+-\w+"; then +if echo "$branch" | grep -Eq "^(feat|fix|refactor|perf|style|test|docs|build|chore|revert)\/\w+(-\w+){0,5}$"; then echo "✅ Branch name valid: $branch" exit 0 else echo "❌ Invalid branch name: $branch" - echo "Branch must be named / (must have 2 stipes - -)" + echo "Branch must be named / (must have one to six words separated by a dash)" exit 1 fi \ No newline at end of file From 46cc5a087d2f4996c9184b143c38f067c042ec6f Mon Sep 17 00:00:00 2001 From: Twirre Meulenbelt <43213592+TwirreM@users.noreply.github.com> Date: Wed, 8 Oct 2025 15:03:44 +0200 Subject: [PATCH 5/6] feat: basic home page Created a basic home page with links, a header with a link to home, some basic reusable CSS classes. ref: N25B-38 --- src/App.css | 137 ++++++++++++++------- src/App.tsx | 20 +-- src/components/components.tsx | 5 +- src/index.css | 18 +-- src/pages/Home/Home.module.css | 18 +++ src/pages/Home/Home.tsx | 47 ++----- src/pages/TemplatePage/Template.module.css | 4 - src/pages/TemplatePage/Template.tsx | 13 -- vite.config.ts | 5 + 9 files changed, 148 insertions(+), 119 deletions(-) delete mode 100644 src/pages/TemplatePage/Template.module.css diff --git a/src/App.css b/src/App.css index dcb46cf..ab28aa0 100644 --- a/src/App.css +++ b/src/App.css @@ -1,24 +1,3 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - - .logopepper { height: 8em; padding: 1.5em; @@ -32,27 +11,21 @@ filter: drop-shadow(0 0 10em #4eff14aa); } -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - - @keyframes logo-pepper-spin { - from { - transform: rotate(-20deg); + 0% { + transform: rotate(0); } - to { + 25% { transform: rotate(20deg); } + 75% { + transform: rotate(-20deg); + } + 100% { + transform: rotate(0); + } } - - @keyframes logo-pepper-scale { from { transform: scale(1,1); @@ -63,19 +36,13 @@ } @media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; + .logopepper:hover { + animation: logo-pepper-spin infinite 1s linear; } } @media (prefers-reduced-motion: no-preference) { - .logopepper { - animation: logo-pepper-spin infinite 1s linear alternate; - } -} - -@media (prefers-reduced-motion: no-preference) { - .logoPepperScaling { + .logoPepperScaling:hover { animation: logo-pepper-scale infinite 1s linear alternate; } } @@ -113,3 +80,83 @@ button.movePage.right{ button.movePage:hover{ background-color: rgb(0, 176, 176); } + + + +header { + position: sticky; + top: 0; + left: 0; + right: 0; + + padding: 1rem; + + display: flex; + gap: 1rem; + align-items: center; + justify-content: center; + + backdrop-filter: blur(10px); + z-index: 1; /* Otherwise any translated elements render above the blur?? */ +} + +main { + padding: 1rem 0; +} + +.flex-row { + display: flex; + flex-direction: row; +} +.flex-col { + display: flex; + flex-direction: column; +} + +.flex-1 { + flex: 1; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.align-center { + align-items: center; +} +.justify-center { + justify-content: center; +} +.justify-between { + justify-content: space-between; +} + +.gap-sm { + gap: .25rem; +} +.gap-md { + gap: .5rem; +} +.gap-lg { + gap: 1rem; +} + +.padding-sm { + padding: .25rem; +} +.padding-md { + padding: .5rem; +} +.padding-lg { + padding: 1rem; +} + +.round-sm { + border-radius: .25rem; +} +.round-md { + border-radius: .5rem; +} +.round-lg { + border-radius: 1rem; +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 80f6643..acec25d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,22 @@ -import { Routes, Route } from 'react-router' +import { Routes, Route, Link } from 'react-router' import './App.css' import TemplatePage from './pages/TemplatePage/Template.tsx' import Home from './pages/Home/Home.tsx' function App(){ - return ( - - } /> - } /> - +
+
+ Home +
+
+ + } /> + } /> + +
+
) } -export default App \ No newline at end of file +export default App diff --git a/src/components/components.tsx b/src/components/components.tsx index d323843..24dd429 100644 --- a/src/components/components.tsx +++ b/src/components/components.tsx @@ -1,14 +1,11 @@ -// src/components/Counter.tsx import { useState } from 'react' -//import style from './Counter.module.css' // optional, if you want a CSS module for reset button -import '../App.css' function Counter() { const [count, setCount] = useState(0) return (
- - - -

- Edit src/App.tsx and save to test HMR -

- -

- Click on the Vite and React logos to learn more -

- +
+ Template → +
+
) } diff --git a/src/pages/TemplatePage/Template.module.css b/src/pages/TemplatePage/Template.module.css deleted file mode 100644 index 8526661..0000000 --- a/src/pages/TemplatePage/Template.module.css +++ /dev/null @@ -1,4 +0,0 @@ -button.reset:hover { - background-color: yellow; -} - diff --git a/src/pages/TemplatePage/Template.tsx b/src/pages/TemplatePage/Template.tsx index 4cb3118..dc24adf 100644 --- a/src/pages/TemplatePage/Template.tsx +++ b/src/pages/TemplatePage/Template.tsx @@ -1,22 +1,9 @@ -import { useState } from 'react' -import { Link } from 'react-router' import Counter from '../../components/components.tsx' -import style from './Template.module.css' - -//this is your css file where you can style your buttons and such -//you can still use css parts from App.css, but also overwrite them function TemplatePage() { - - return ( <> - {/* here you link to the homepage, in App.tsx you can link new pages */} - - ) } diff --git a/vite.config.ts b/vite.config.ts index 8b0f57b..aa7de4f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,4 +4,9 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + css: { + modules: { + localsConvention: "camelCase", + } + }, }) From d8ed8df9826b3de48a0b50fd7293577fb1a49eb3 Mon Sep 17 00:00:00 2001 From: Twirre Meulenbelt <43213592+TwirreM@users.noreply.github.com> Date: Wed, 8 Oct 2025 15:56:34 +0200 Subject: [PATCH 6/6] feat: update robot interaction page This page is now fancier, shows messages streaming from the Control Backend. ref: N25B-164 --- src/App.tsx | 2 + src/pages/Home/Home.tsx | 1 + src/pages/Robot/Robot.tsx | 94 +++++++++++++++++++++++++++ src/pages/ServerComms/ServerComms.css | 0 src/pages/ServerComms/ServerComms.tsx | 80 ----------------------- 5 files changed, 97 insertions(+), 80 deletions(-) create mode 100644 src/pages/Robot/Robot.tsx delete mode 100644 src/pages/ServerComms/ServerComms.css delete mode 100644 src/pages/ServerComms/ServerComms.tsx diff --git a/src/App.tsx b/src/App.tsx index acec25d..803b84c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { Routes, Route, Link } from 'react-router' import './App.css' import TemplatePage from './pages/TemplatePage/Template.tsx' import Home from './pages/Home/Home.tsx' +import Robot from './pages/Robot/Robot.tsx'; function App(){ return ( @@ -13,6 +14,7 @@ function App(){ } /> } /> + } /> diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 71f6b96..cb70de0 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -11,6 +11,7 @@ function Home() {
+ Robot Interaction → Template →
diff --git a/src/pages/Robot/Robot.tsx b/src/pages/Robot/Robot.tsx new file mode 100644 index 0000000..0038dd9 --- /dev/null +++ b/src/pages/Robot/Robot.tsx @@ -0,0 +1,94 @@ +import { useState, useEffect, useRef } from 'react' + +export default function Robot() { + const [message, setMessage] = useState(''); + + const [listening, setListening] = useState(false); + const [conversation, setConversation] = useState<{"role": "user" | "assistant", "content": string}[]>([]) + const conversationRef = useRef(null); + const [conversationIndex, setConversationIndex] = useState(0); + + const sendMessage = async () => { + try { + const response = await fetch("http://localhost:8000/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); + } + }; + + useEffect(() => { + const eventSource = new EventSource("http://localhost:8000/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]); + + 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/src/pages/ServerComms/ServerComms.css b/src/pages/ServerComms/ServerComms.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/pages/ServerComms/ServerComms.tsx b/src/pages/ServerComms/ServerComms.tsx deleted file mode 100644 index c16ec81..0000000 --- a/src/pages/ServerComms/ServerComms.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useState, useEffect } from 'react' -import { Link } from 'react-router' -//import Counter from '../../components/components.tsx' - - -//this is your css file where you can style your buttons and such -//you can still use css parts from App.css, but also overwrite them - -function ServerComms() { - const [message, setMessage] = useState(''); - const [sseMessage, setSseMessage] = useState(''); - const [spoken, setSpoken] = useState(""); - - const sendMessage = async () => { - try { - const response = await fetch("http://localhost:8000/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); - } - }; - - useEffect(() => { - const eventSource = new EventSource("http://localhost:8000/sse"); - - eventSource.onmessage = (event) => { - setSseMessage(event.data); - - try { - const data = JSON.parse(event.data); - if (data.speech) setSpoken(data.speech); - } catch {} - }; - - return () => { - eventSource.close(); - }; - }, []); - - return ( -
-
- setMessage(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && sendMessage().then(() => setMessage(""))} - placeholder="Enter a message" - /> - -
-
-

Message from Server (SSE):

-

{sseMessage}

-
-
-

Spoken text (SSE):

-

{spoken}

-
-
- {/* here you link to the homepage, in App.tsx you can link new pages */} - - -
-
- - - ); -} - -export default ServerComms \ No newline at end of file