diff --git a/Copilot_20250822_144055.png b/Copilot_20250822_144055.png new file mode 100644 index 0000000..9632075 Binary files /dev/null and b/Copilot_20250822_144055.png differ diff --git a/README.md b/README.md index 0c59772..b5b4b3f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ La soluzione è basata sul seguente stack * Abilitazione LM Studio x chiamate locali su porta 1234 * virtual machine linux con soluzione backend/frontend di proxy/caching verso il modello AI di LM Studio +![alt text](Copilot_20250822_144055.png) + ## Startup Al momento per l0'esecuzione della soluzione, sulla virtual machine di proxy, vanno avviati backend (python) e frontend (node) manualmente. @@ -66,4 +68,5 @@ Mancano molti punti di ottimizzazione: |---------------|-----------------------------------------|------------| | 0.1.2508.2019 | Versione test solo locale con LM Studio | 2025.08.20 | | 0.1.2508.2119 | Versione con esecuzione locale completa | 2025.08.21 | +| 0.1.2508.2219 | Versione completa e rivisitata graficamente x chat (con memoria sessioni) | 2025.08.22 | diff --git a/README.pdf b/README.pdf index 93b75ff..2f06f11 100644 Binary files a/README.pdf and b/README.pdf differ diff --git a/frontend/src/App.css b/frontend/src/App.css index d83407f..fcb31c2 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,11 +1,90 @@ /* src/App.css */ -body, html { +html, body, #root { + height: 100%; margin: 0; padding: 0; - height: 100%; + overflow: hidden; +} + +body { + background-color: var(--bs-body-bg); font-family: sans-serif; } +/* Animate only the wrapper, not the whole page */ +.sessions-wrapper { + transition: transform 300ms ease, filter 300ms ease; + will-change: transform, filter; +} +body.sessions-open .sessions-wrapper { + transform: translateX(280px) scale(0.985); + filter: saturate(0.95); +} + +/* Offcanvas stays fixed to the viewport */ +.offcanvas-start { + width: 280px; + border-right: 1px solid #dee2e6; +} +.offcanvas.show { + box-shadow: 6px 0 20px rgba(0, 0, 0, 0.08); +} + +/* Dark overlay for sessions panel */ +.sessions-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + opacity: 0; + pointer-events: none; + transition: opacity 250ms ease; + z-index: 1030; /* just under offcanvas (Bootstrap sets .offcanvas at 1045) */ +} +.sessions-overlay.show { + opacity: 1; + pointer-events: auto; +} + +/* Animate only wrapper */ +.sessions-wrapper { + transition: transform 300ms ease, filter 300ms ease; + will-change: transform, filter; +} +body.sessions-open .sessions-wrapper { + transform: translateX(280px) scale(0.985); + filter: saturate(0.95); +} + +.session-table { + font-size: 0.85rem; +} + +.offcanvas-start { + width: 420px; + border-right: 1px solid #dee2e6; +} +.offcanvas.show { + box-shadow: 6px 0 20px rgba(0, 0, 0, 0.08); +} + +.offcanvas-left { + position: fixed; + top: 0; + left: -300px; /* panel width */ + width: 300px; + height: 100%; + background: var(--bs-body-bg, #fff); + box-shadow: 2px 0 5px rgba(0,0,0,0.3); + transition: left 0.3s ease; + z-index: 1050; +} + +.offcanvas-left.show { + left: 0; +} + + +/* Chat styles */ .chat-container { display: flex; flex-direction: column; @@ -17,40 +96,34 @@ body, html { flex: 1; overflow-y: auto; padding: 1rem; - background-color: #f8f9fa; /* Bootstrap light gray */ - overflow-anchor: none; /* prevent anchor jumps */ + background-color: #f8f9fa; + overflow-anchor: none; } -/* Wider bubbles — use Bootstrap breakpoints for responsiveness */ .message { margin: 0.5rem 0; padding: 0.9rem 1rem; border-radius: 12px; width: auto; - max-width: 95%; /* was 80% */ + max-width: 95%; word-wrap: break-word; } - .message:last-child { - overflow-anchor: auto; /* anchor here */ + overflow-anchor: auto; } - @media (min-width: 768px) { - .message { - max-width: 75%; /* keep some margin on larger screens */ - } + .message { max-width: 75%; } } .message.user { - background-color: #0d6efd; /* Bootstrap primary */ + background-color: #0d6efd; color: #fff; align-self: flex-end; text-align: right; - border-top-right-radius: 0; /* subtle speech bubble effect */ + border-top-right-radius: 0; } - .message.assistant { - background-color: #e9ecef; /* Bootstrap light gray */ + background-color: #e9ecef; color: #212529; align-self: flex-start; text-align: left; @@ -63,7 +136,6 @@ body, html { background-color: white; border-top: 1px solid #ccc; } - .chat-input { flex: 1; padding: 0.75rem; @@ -71,11 +143,10 @@ body, html { border-radius: 8px; font-size: 1rem; } - .send-button { margin-left: 0.5rem; padding: 0.75rem 1rem; - background-color: #0d6efd; /* Bootstrap primary */ + background-color: #0d6efd; color: white; border: none; border-radius: 8px; @@ -83,18 +154,17 @@ body, html { } pre { - background-color: #212529; /* dark background for code */ + background-color: #212529; color: #f8f9fa; padding: 0.75rem; border-radius: 0.375rem; overflow-x: auto; } - code { font-family: 'Fira Code', monospace; font-size: 0.9rem; } - .btn-copy { font-size: 0.75rem; } + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 9dcd044..d26d7de 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4,7 +4,7 @@ import "./App.css"; import { themes } from "./themes"; import ChatLayout from "./ChatLayout"; import { useChatStream } from "./useChatStream"; -import { getSessionId, getUserId, resetSessionId } from "./useSessionId"; +import { getSessionId, getUserId, resetSessionId, setSessionId } from "./useSessionId"; export default function App() { const { messages, loading, sendMessage, stopGenerating, setMessages } = useChatStream(); @@ -22,23 +22,46 @@ export default function App() { localStorage.setItem("preferredTheme", themeName); }, [themeName]); - const toggleTheme = () => { - setThemeName((t) => (t === "light" ? "dark" : "light")); - }; + const toggleTheme = () => setThemeName(t => (t === "light" ? "dark" : "light")); - const reloadHistory = async () => { - const res = await fetch(`/v1/history?user_id=${userId}&session_id=${sessionId}`); + const reloadHistory = async (id = sessionId) => { + const res = await fetch(`/v1/history?user_id=${userId}&session_id=${id}`); const history = await res.json(); - setMessages(history); // from useChatStream + setMessages(history); }; const freshStart = async () => { await fetch(`/v1/history?user_id=${userId}&session_id=${sessionId}`, { method: "DELETE" }); setMessages([]); - resetSessionId(); - // or start a brand new sessionId: - //localStorage.removeItem("sessionId"); - //window.location.reload(); + const newId = resetSessionId(); + setSessionId(newId); + }; + + const createSession = async () => { + const firstMessage = prompt("Enter a name or first message for the new session:") || ""; + const res = await fetch(`/v1/sessions?user_id=${userId}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ first_message: firstMessage }), + }); + const meta = await res.json(); + setSessionId(meta.session_id); + setMessages([]); + }; + + const editSession = async () => { + const newName = prompt("Enter a new name for this session:"); + if (!newName) return; + await fetch(`/v1/sessions/${sessionId}?user_id=${userId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ session_name: newName }), + }); + }; + + const handleSelectSession = async (selectedId) => { + setSessionId(selectedId); + await reloadHistory(selectedId); }; return ( @@ -49,10 +72,13 @@ export default function App() { onSend={sendMessage} onStop={stopGenerating} onToggleTheme={toggleTheme} - onReloadHistory={reloadHistory} + onReloadHistory={() => reloadHistory()} onFreshStart={freshStart} + onCreateSession={createSession} + onEditSession={editSession} + onSelectSession={handleSelectSession} + userId={userId} /> ); } - diff --git a/frontend/src/App.jsx.old b/frontend/src/App.jsx.old new file mode 100644 index 0000000..9541539 --- /dev/null +++ b/frontend/src/App.jsx.old @@ -0,0 +1,81 @@ +//App.jsx +import React, { useState, useEffect } from "react"; +import "./App.css"; +import { themes } from "./themes"; +import ChatLayout from "./ChatLayout"; +import { useChatStream } from "./useChatStream"; +import { getSessionId, getUserId, resetSessionId } from "./useSessionId"; + +export default function App() { + const { messages, loading, sendMessage, stopGenerating, setMessages } = useChatStream(); + const [themeName, setThemeName] = useState("light"); + const theme = themes[themeName]; + const sessionId = getSessionId(); + const userId = getUserId(); + + useEffect(() => { + const saved = localStorage.getItem("preferredTheme"); + if (saved && themes[saved]) setThemeName(saved); + }, []); + + useEffect(() => { + localStorage.setItem("preferredTheme", themeName); + }, [themeName]); + + const toggleTheme = () => { + setThemeName((t) => (t === "light" ? "dark" : "light")); + }; + + const reloadHistory = async () => { + const res = await fetch(`/v1/history?user_id=${userId}&session_id=${sessionId}`); + const history = await res.json(); + setMessages(history); // from useChatStream + }; + + const freshStart = async () => { + await fetch(`/v1/history?user_id=${userId}&session_id=${sessionId}`, { method: "DELETE" }); + setMessages([]); + resetSessionId(); + // or start a brand new sessionId: + //localStorage.removeItem("sessionId"); + //window.location.reload(); + }; + + const createSession = async () => { + const firstMessage = prompt("Enter a name or first message for the new session:") || ""; + const res = await fetch(`/v1/sessions?user_id=${userId}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ first_message: firstMessage }), + }); + const meta = await res.json(); + setSessionId(meta.session_id); + setMessages([]); // clear chat window + }; + + const editSession = async () => { + const newName = prompt("Enter a new name for this session:"); + if (!newName) return; + await fetch(`/v1/sessions/${sessionId}?user_id=${userId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ session_name: newName }), + }); + }; + + return ( + + ); +} + + diff --git a/frontend/src/App.jsx.orig b/frontend/src/App.jsx.orig deleted file mode 100644 index d3d874b..0000000 --- a/frontend/src/App.jsx.orig +++ /dev/null @@ -1,177 +0,0 @@ -// src/App.jsx -import React, { useState, useRef, useEffect } from 'react'; -import ReactMarkdown from 'react-markdown'; -import remarkMath from 'remark-math'; -import rehypeKatex from 'rehype-katex'; -import MessageContent from './MessageContent'; -import { useStreamBuffer } from './hooks/useStreamBuffer' -import './App.css'; -import 'katex/dist/katex.min.css'; - - -function App() { - const [messages, setMessages] = useState([]); - const [input, setInput] = useState(""); - const [loading, setLoading] = useState(false); - const messagesEndRef = useRef(null); - const { buffered, pushChunk, reset } = useStreamBuffer(80); - const sendMessage = async () => { - if (!input.trim() || loading) return; - - const userMessage = { role: "user", content: input }; - const userId = "user1"; - - // Calculate where the assistant placeholder will land - const startIndex = messages.length; - const assistantIndex = startIndex + 1; - - // Optimistic UI: user + empty assistant - setMessages(prev => [...prev, userMessage, { role: "assistant", content: "" }]); - setInput(""); - setLoading(true); - reset(); // clear the buffer for the new response - - 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 }) - }); - - // Non-streaming fallback - if (!res.ok || !res.body) { - const json = await res.json().catch(() => null); - const text = json?.response ?? "Error: streaming not available."; - setMessages(prev => { - const next = [...prev]; - next[assistantIndex] = { role: "assistant", content: text }; - return next; - }); - setLoading(false); - return; - } - - const reader = res.body.getReader(); - const decoder = new TextDecoder("utf-8"); - let acc = ""; // final committed text - let sseBuffer = ""; // raw SSE buffer - - while (true) { - const { value, done } = await reader.read(); - if (done) break; - - sseBuffer += decoder.decode(value, { stream: true }); - - // Split on SSE event boundaries - const events = sseBuffer.split("\n\n"); - sseBuffer = events.pop() || ""; - - for (const evt of events) { - const lines = evt.split("\n").map(l => l.trim()).filter(Boolean); - for (const line of lines) { - if (!line.startsWith("data:")) continue; - const data = line.slice(5).trim(); - if (data === "[DONE]") { - // Commit final text and finish - 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; // reliable final copy - pushChunk(piece); // smooth UI copy - } - } catch { - // ignore non-JSON control lines - } - } - } - } - - // Stream ended without an explicit [DONE] - setMessages(prev => { - const next = [...prev]; - next[assistantIndex] = { role: "assistant", content: acc }; - return next; - }); - } catch (err) { - setMessages(prev => { - const next = [...prev]; - next[assistantIndex] = { role: "assistant", content: `Error: ${String(err)}` }; - return next; - }); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - if (loading) { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - } - }, [buffered, loading]); - - return ( -
-
-
- Egalware's LM Studio Chat -
-
- -
- {messages.map((msg, i) => { - const isLastAssistant = i === messages.length - 1 && msg.role === "assistant" && loading; - return ( -
-
-
- -
-
-
- ); - })} - - {loading && ( -
-
-
- The model is processing... -
-
- )} - -
-
- -
-
- setInput(e.target.value)} - onKeyDown={e => e.key === "Enter" && sendMessage()} - placeholder="Type your message..." - disabled={loading} - autoFocus - /> - -
-
-
- ); -} - -export default App; diff --git a/frontend/src/ChatHeader.jsx b/frontend/src/ChatHeader.jsx index 8e57116..2166512 100644 --- a/frontend/src/ChatHeader.jsx +++ b/frontend/src/ChatHeader.jsx @@ -1,29 +1,50 @@ -// ChatHeader.jsx +// src/ChatHeader.jsx import React from "react"; export default function ChatHeader({ theme, onToggleTheme, onReloadHistory, - onFreshStart + onFreshStart, + onCreateSession, + onManageSession }) { return (
- {/* Left column: new buttons */} -
+ {/* Left column: control buttons */} +
+ + + + +
@@ -33,16 +54,16 @@ export default function ChatHeader({
{/* Right column: theme toggle */} -
+
); } - diff --git a/frontend/src/ChatLayout.jsx b/frontend/src/ChatLayout.jsx index a862217..a53fb45 100644 --- a/frontend/src/ChatLayout.jsx +++ b/frontend/src/ChatLayout.jsx @@ -1,8 +1,9 @@ -// ChatLayout.jsx -import React from "react"; +// App.jsx +import React, { useState, useEffect } from "react"; import ChatHeader from "./ChatHeader"; import ChatWindow from "./ChatWindow"; import ChatInput from "./ChatInput"; +import SessionTable from "./SessionTable"; export default function ChatLayout({ theme, @@ -12,27 +13,88 @@ export default function ChatLayout({ onStop, onToggleTheme, onReloadHistory, - onFreshStart + onFreshStart, + onCreateSession, + onEditSession, + onSelectSession, + userId }) { + const [showSessionsPanel, setShowSessionsPanel] = useState(false); + + // 1️⃣ helper at the top (inside the component is fine) + function getScrollbarWidth() { + return window.innerWidth - document.documentElement.clientWidth; + } + + // update body class when panel state changes + useEffect(() => { + document.body.classList.toggle("sessions-open", showSessionsPanel); + }, [showSessionsPanel]); + + // 2️⃣ revised toggles + const openSessionManager = () => { + const scrollBarWidth = getScrollbarWidth(); + document.body.style.overflow = "hidden"; + document.body.style.paddingRight = `${scrollBarWidth}px`; + setShowSessionsPanel(true); + }; + + const closeSessionManager = () => { + document.body.style.overflow = ""; + document.body.style.paddingRight = ""; + setShowSessionsPanel(false); + }; + return ( -
- - + <> +
+ - {loading && ( -
- +
+
- )} - -
+ {loading && ( +
+ +
+ )} + + +
+ +
+
+
Manage Sessions
+ +
+
+ +
+
+ ); } + diff --git a/frontend/src/ChatLayout.jsx.orig b/frontend/src/ChatLayout.jsx.orig new file mode 100644 index 0000000..2b29f0f --- /dev/null +++ b/frontend/src/ChatLayout.jsx.orig @@ -0,0 +1,79 @@ +// src/ChatLayout.jsx +import React, { useState } from "react"; +import ChatHeader from "./ChatHeader"; +import ChatWindow from "./ChatWindow"; +import ChatInput from "./ChatInput"; +import SessionTable from "./SessionTable"; // your existing table + +export default function ChatLayout({ + theme, + messages, + loading, + onSend, + onStop, + onToggleTheme, + onReloadHistory, + onFreshStart, + onCreateSession, + onSelectSession, // new: load a chosen session + userId // new: so SessionTable can fetch sessions +}) { + const [showSessionsPanel, setShowSessionsPanel] = useState(false); + + const openSessionManager = () => setShowSessionsPanel(true); + const closeSessionManager = () => setShowSessionsPanel(false); + + return ( +
+ + +
+ +
+ + {loading && ( +
+ +
+ )} + + + + {/* Offcanvas Session Manager */} +
+
+
Manage Sessions
+ +
+
+ { + onSelectSession(sessionId); // tell parent to load history + closeSessionManager(); + }} + /> +
+
+
+ ); +} + + diff --git a/frontend/src/SessionTable.jsx b/frontend/src/SessionTable.jsx new file mode 100644 index 0000000..4ce508d --- /dev/null +++ b/frontend/src/SessionTable.jsx @@ -0,0 +1,133 @@ +// src/SessionTable.jsx +import React, { useEffect, useState } from "react"; +import { getSessionId } from "./useSessionId"; + +export default function SessionTable({ userId, onSelectSession, onClosePanel }) { + const [sessions, setSessions] = useState([]); + const [loading, setLoading] = useState(false); + const [editingSessionId, setEditingSessionId] = useState(null); + const [editName, setEditName] = useState(""); + const activeSessionId = getSessionId(); + + const fetchSessions = async () => { + setLoading(true); + try { + const res = await fetch(`/v1/sessions?user_id=${userId}`); + const data = await res.json(); + setSessions(data); + } catch (err) { + console.error("Failed to fetch sessions", err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (userId) fetchSessions(); + }, [userId]); + + const startEditing = (session) => { + setEditingSessionId(session.session_id); + setEditName(session.session_name); + }; + + const saveName = async (sessionId) => { + try { + await fetch(`/v1/sessions/${sessionId}?user_id=${userId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ session_name: editName }), + }); + setEditingSessionId(null); + fetchSessions(); + } catch (err) { + console.error("Failed to rename session", err); + } + }; + + const deleteSession = async (sessionId) => { + if (!window.confirm("Delete this session and its history?")) return; + try { + await fetch(`/v1/sessions/${sessionId}?user_id=${userId}`, { method: "DELETE" }); + fetchSessions(); + } catch (err) { + console.error("Failed to delete session", err); + } + }; + + return ( +
+ + + + + + + + + + + + {loading && } + {!loading && sessions.length === 0 && ( + + )} + {!loading && sessions.map((s) => ( + + + + + + + + ))} + +
SessionCreated# mess
Loading…
No sessions found
+ + + {editingSessionId === s.session_id ? ( + setEditName(e.target.value)} + onBlur={() => saveName(s.session_id)} + onKeyDown={(e) => { + if (e.key === "Enter") saveName(s.session_id); + if (e.key === "Escape") setEditingSessionId(null); + }} + autoFocus + /> + ) : ( + { + onSelectSession(s.session_id); + if (typeof onClosePanel === "function") onClosePanel(); + }} + title="Click to load this session" + > + {s.session_name} + + )} + {new Date(s.created_at).toLocaleString()} + {s.message_count} + + +
+
+ ); +} + diff --git a/frontend/src/UserMessage.jsx b/frontend/src/UserMessage.jsx index 9488b52..52a968e 100644 --- a/frontend/src/UserMessage.jsx +++ b/frontend/src/UserMessage.jsx @@ -3,18 +3,22 @@ import React from "react"; export default function UserMessage({ content, theme }) { return ( -
+ // This outer div aligns the *bubble* to the right +
 background
-            color: "inherit", // match bubble text color
+            backgroundColor: "transparent",
+            color: "inherit",
           }}
         >
           {content}
diff --git a/frontend/src/themes.js b/frontend/src/themes.js
index 0bd113f..a3e6300 100644
--- a/frontend/src/themes.js
+++ b/frontend/src/themes.js
@@ -1,13 +1,13 @@
 // themes.js
 export const themes = {
   light: {
-    userBg: "bg-primary text-white",
+    userBg: "bg-primary bg-opacity-25 text-dark",
     assistantBg: "bg-white border text-dark",
     bodyBg: "bg-light",
     headerBg: "bg-primary text-white",
   },
   dark: {
-    userBg: "bg-dark text-white",
+    userBg: "bg-primary text-white",
     assistantBg: "bg-secondary text-white",
     bodyBg: "bg-black text-white",
     headerBg: "bg-dark text-white",
diff --git a/frontend/src/useChatStream.js b/frontend/src/useChatStream.js
index 02a95ad..0745b93 100644
--- a/frontend/src/useChatStream.js
+++ b/frontend/src/useChatStream.js
@@ -6,18 +6,18 @@ export function useChatStream() {
   const [messages, setMessages] = useState([]);
   const [loading, setLoading] = useState(false);
   const abortRef = useRef(null);
-  const sessionId = getSessionId();
+
   const userId = getUserId();
 
   const sendMessage = useCallback(
     async (input) => {
       if (!input.trim()) return;
 
-      // Abort any previous stream
+      const sessionId = getSessionId(); // <-- get latest every time
+
       if (abortRef.current) {
         abortRef.current.abort();
       }
-
       const controller = new AbortController();
       abortRef.current = controller;
 
@@ -33,7 +33,7 @@ export function useChatStream() {
             "Content-Type": "application/json",
             Accept: "text/event-stream",
           },
-	  body: JSON.stringify({ user_id: userId, session_id: sessionId, message: input }),
+          body: JSON.stringify({ user_id: userId, session_id: sessionId, message: input }),
           signal: controller.signal,
         });
 
@@ -101,7 +101,7 @@ export function useChatStream() {
         abortRef.current = null;
       }
     },
-    [messages]
+    [messages, userId]
   );
 
   const stopGenerating = useCallback(() => {
@@ -115,4 +115,3 @@ export function useChatStream() {
   return { messages, loading, sendMessage, stopGenerating, setMessages };
 }
 
-
diff --git a/frontend/src/useSessionId.js b/frontend/src/useSessionId.js
index 9ac49f8..7371607 100644
--- a/frontend/src/useSessionId.js
+++ b/frontend/src/useSessionId.js
@@ -1,4 +1,4 @@
-// useIds.js
+// useSessionId.js
 export function getUserId() {
   let id = localStorage.getItem("userId");
   if (!id) {
@@ -9,6 +9,7 @@ export function getUserId() {
 }
 
 export function getSessionId() {
+  // Always re‑read localStorage so latest value is returned
   let id = localStorage.getItem("sessionId");
   if (!id) {
     id = crypto.randomUUID();
@@ -17,6 +18,10 @@ export function getSessionId() {
   return id;
 }
 
+export function setSessionId(id) {
+  localStorage.setItem("sessionId", id);
+}
+
 export function resetSessionId() {
   const newId = crypto.randomUUID();
   localStorage.setItem("sessionId", newId);