From c955db7fa61c359fb9c39aca5594f08e94db6c02 Mon Sep 17 00:00:00 2001 From: "Samuele E. Locatelli" Date: Fri, 22 Aug 2025 08:45:57 +0000 Subject: [PATCH] Update on frontend con single user session --- frontend/src/App.jsx | 126 ++++++---------------------------- frontend/src/ChatHeader.jsx | 19 +++++ frontend/src/ChatLayout.jsx | 33 +++++++++ frontend/src/UserMessage.jsx | 8 ++- frontend/src/useChatStream.js | 115 +++++++++++++++++++++++++++++++ 5 files changed, 194 insertions(+), 107 deletions(-) create mode 100644 frontend/src/ChatHeader.jsx create mode 100644 frontend/src/ChatLayout.jsx create mode 100644 frontend/src/useChatStream.js diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index da87586..0f3cd78 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,123 +1,37 @@ -import React, { useState } from "react"; +// App.jsx +import React, { useState, useEffect } from "react"; import "./App.css"; import { themes } from "./themes"; -import ChatWindow from "./ChatWindow"; -import ChatInput from "./ChatInput"; +import ChatLayout from "./ChatLayout"; +import { useChatStream } from "./useChatStream"; export default function App() { - const [messages, setMessages] = useState([]); - const [loading, setLoading] = useState(false); + const { messages, loading, sendMessage, stopGenerating } = useChatStream(); const [themeName, setThemeName] = useState("light"); - const theme = themes[themeName]; - const sendMessage = async (input) => { - if (!input.trim() || loading) return; + useEffect(() => { + const saved = localStorage.getItem("preferredTheme"); + if (saved && themes[saved]) setThemeName(saved); + }, []); - const userMessage = { role: "user", content: input }; - const userId = "userTest"; - const assistantIndex = messages.length + 1; - - setMessages((prev) => [ - ...prev, - userMessage, - { role: "assistant", content: "" }, - ]); - setLoading(true); - - try { - const res = await fetch("/v1/chat-stream", { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "text/event-stream", - }, - body: JSON.stringify({ user_id: userId, message: userMessage.content }), - }); - - if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`); - - const reader = res.body.getReader(); - const decoder = new TextDecoder("utf-8"); - let buffer = ""; - let acc = ""; - - while (true) { - const { value, done } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - const parts = buffer.split("\n\n"); - buffer = parts.pop() || ""; - for (const part of parts) { - const line = part - .split("\n") - .map((l) => l.trim()) - .find((l) => l.startsWith("data:")); - if (!line) continue; - const data = line.slice(5).trim(); - if (data === "[DONE]") { - setMessages((prev) => { - const next = [...prev]; - next[assistantIndex] = { role: "assistant", content: acc }; - return next; - }); - setLoading(false); - return; - } - try { - const obj = JSON.parse(data); - const choice = obj?.choices?.[0] ?? {}; - const delta = choice.delta ?? {}; - const piece = delta.content ?? choice.text ?? ""; - if (piece) { - acc += piece; - setMessages((prev) => { - const next = [...prev]; - next[assistantIndex] = { role: "assistant", content: acc }; - return next; - }); - } - } catch { - // ignore malformed chunk - } - } - } - } catch (err) { - setMessages((prev) => { - const next = [...prev]; - next[assistantIndex] = { - role: "assistant", - content: `Error: ${String(err)}`, - }; - return next; - }); - } finally { - setLoading(false); - } - }; + useEffect(() => { + localStorage.setItem("preferredTheme", themeName); + }, [themeName]); const toggleTheme = () => { setThemeName((t) => (t === "light" ? "dark" : "light")); }; return ( -
-
-
-
-
🤖 EgalWare's LLM ChatBot
-
-
- -
-
- - - - -
+ ); } diff --git a/frontend/src/ChatHeader.jsx b/frontend/src/ChatHeader.jsx new file mode 100644 index 0000000..50d3c5e --- /dev/null +++ b/frontend/src/ChatHeader.jsx @@ -0,0 +1,19 @@ +// ChatHeader.jsx +import React from "react"; + +export default function ChatHeader({ theme, onToggleTheme }) { + return ( +
+
+
+

🤖 EgalWare's LLM ChatBot

+
+
+ +
+
+ ); +} + diff --git a/frontend/src/ChatLayout.jsx b/frontend/src/ChatLayout.jsx new file mode 100644 index 0000000..4645297 --- /dev/null +++ b/frontend/src/ChatLayout.jsx @@ -0,0 +1,33 @@ +// ChatLayout.jsx +import React from "react"; +import ChatHeader from "./ChatHeader"; +import ChatWindow from "./ChatWindow"; +import ChatInput from "./ChatInput"; + +export default function ChatLayout({ + theme, + messages, + loading, + onSend, + onStop, + onToggleTheme +}) { + return ( +
+ + + + {loading && ( +
+ +
+ )} + + +
+ ); +} + + diff --git a/frontend/src/UserMessage.jsx b/frontend/src/UserMessage.jsx index 67fcbf4..9488b52 100644 --- a/frontend/src/UserMessage.jsx +++ b/frontend/src/UserMessage.jsx @@ -1,3 +1,4 @@ +// UserMessage.jsx import React from "react"; export default function UserMessage({ content, theme }) { @@ -9,7 +10,12 @@ export default function UserMessage({ content, theme }) { >
 background
+            color: "inherit", // match bubble text color
+          }}
         >
           {content}
         
diff --git a/frontend/src/useChatStream.js b/frontend/src/useChatStream.js new file mode 100644 index 0000000..56147e6 --- /dev/null +++ b/frontend/src/useChatStream.js @@ -0,0 +1,115 @@ +// useChatStream.js +import { useState, useCallback, useRef } from "react"; + +export function useChatStream() { + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(false); + const abortRef = useRef(null); + + const sendMessage = useCallback( + async (input) => { + if (!input.trim()) return; + + // Abort any previous stream + if (abortRef.current) { + abortRef.current.abort(); + } + + const controller = new AbortController(); + abortRef.current = controller; + + const userMessage = { role: "user", content: input }; + const assistantIndex = messages.length + 1; + setMessages((prev) => [...prev, userMessage, { role: "assistant", content: "" }]); + setLoading(true); + + try { + const res = await fetch("/v1/chat-stream", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "text/event-stream", + }, + body: JSON.stringify({ user_id: "userTest", message: input }), + signal: controller.signal, + }); + + if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`); + + const reader = res.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let buffer = ""; + let acc = ""; + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const parts = buffer.split("\n\n"); + buffer = parts.pop() || ""; + + for (const part of parts) { + const line = part + .split("\n") + .map((l) => l.trim()) + .find((l) => l.startsWith("data:")); + if (!line) continue; + + const data = line.slice(5).trim(); + if (data === "[DONE]") { + setMessages((prev) => { + const next = [...prev]; + next[assistantIndex] = { role: "assistant", content: acc }; + return next; + }); + setLoading(false); + return; + } + + try { + const obj = JSON.parse(data); + const piece = obj?.choices?.[0]?.delta?.content ?? obj?.choices?.[0]?.text ?? ""; + if (piece) { + acc += piece; + setMessages((prev) => { + const next = [...prev]; + next[assistantIndex] = { role: "assistant", content: acc }; + return next; + }); + } + } catch { + /* ignore malformed JSON chunks */ + } + } + } + } catch (err) { + if (err.name !== "AbortError") { + setMessages((prev) => { + const next = [...prev]; + next[assistantIndex] = { + role: "assistant", + content: `Error: ${String(err)}`, + }; + return next; + }); + } + } finally { + setLoading(false); + abortRef.current = null; + } + }, + [messages] + ); + + const stopGenerating = useCallback(() => { + if (abortRef.current) { + abortRef.current.abort(); + abortRef.current = null; + setLoading(false); + } + }, []); + + return { messages, loading, sendMessage, stopGenerating }; +} + +