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
+
+
## 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 (
+
| + | Session | +Created | +# 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} + | ++ + | +
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);