Fix comportamento grafico chat

This commit is contained in:
Samuele E. Locatelli
2025-08-22 15:44:34 +00:00
parent df6561be7c
commit 5481c1f4b7
6 changed files with 411 additions and 61 deletions
+68 -28
View File
@@ -1,23 +1,72 @@
/* src/App.css */
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden; /* avoid double scrollbars */
overflow: hidden;
}
body {
background-color: var(--bs-body-bg);
}
body, html {
margin: 0;
padding: 0;
height: 100%;
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);
}
.offcanvas-start {
width: 280px;
border-right: 1px solid #dee2e6;
}
.offcanvas.show {
box-shadow: 6px 0 20px rgba(0, 0, 0, 0.08);
}
/* rest of your existing chat styles unchanged... */
/* Chat styles */
.chat-container {
display: flex;
flex-direction: column;
@@ -29,40 +78,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;
@@ -75,7 +118,6 @@ body, html {
background-color: white;
border-top: 1px solid #ccc;
}
.chat-input {
flex: 1;
padding: 0.75rem;
@@ -83,11 +125,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;
@@ -95,18 +136,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;
}
+23
View File
@@ -41,6 +41,28 @@ export default function App() {
//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 (
<ChatLayout
theme={theme}
@@ -51,6 +73,7 @@ export default function App() {
onToggleTheme={toggleTheme}
onReloadHistory={reloadHistory}
onFreshStart={freshStart}
onCreateSession={createSession}
/>
);
}
+31 -11
View File
@@ -5,45 +5,65 @@ export default function ChatHeader({
theme,
onToggleTheme,
onReloadHistory,
onFreshStart
onFreshStart,
onCreateSession,
onManageSession
}) {
return (
<header
className={`${theme.headerBg} p-2 sticky-top shadow row align-items-center`}
>
{/* Left column: new buttons */}
<div className="col-3 d-flex gap-2 justify-content-start">
{/* Left column: control buttons */}
<div className="col-5 d-flex flex-wrap gap-2 justify-content-start">
<button
className="btn btn-sm btn-outline-light"
onClick={onReloadHistory}
title="Reload current session history"
>
🔄 Reload
🔄
</button>
<button
className="btn btn-sm btn-outline-warning"
className="btn btn-sm btn-warning"
onClick={onFreshStart}
title="Reset/Clear current session and start fresh"
>
🆕 Reset
🆕
</button>
<button
className="btn btn-sm btn-success"
onClick={onCreateSession}
title="Create a brand new session"
>
</button>
<button
className="btn btn-sm btn-info"
onClick={onManageSession}
title="Manage your chat sessions"
>
📂
</button>
</div>
{/* Center column: title */}
<div className="col-6 text-center">
<div className="col-4 text-center">
<h4 className="mb-0">🤖 EgalWare&apos;s LLM ChatBot</h4>
</div>
{/* Right column: theme toggle */}
<div className="col-3 text-end">
<button
className="btn btn-sm btn-outline-light"
className="btn btn-sm btn-outline-light d-flex align-items-center gap-1"
onClick={onToggleTheme}
title="Toggle light/dark theme"
>
<span role="img" aria-label="theme toggle icon">🌓</span>
Theme
<span role="img" aria-label="theme toggle icon">🌓</span>
</button>
</div>
</header>
);
}
+72 -22
View File
@@ -1,9 +1,9 @@
// src/ChatLayout.jsx
// 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,
@@ -13,29 +13,79 @@ export default function ChatLayout({
onStop,
onToggleTheme,
onReloadHistory,
onFreshStart
onFreshStart,
onCreateSession,
onSelectSession,
userId
}) {
const [showSessionsPanel, setShowSessionsPanel] = useState(false);
useEffect(() => {
document.body.classList.toggle("sessions-open", showSessionsPanel);
}, [showSessionsPanel]);
const openSessionManager = () => setShowSessionsPanel(true);
const closeSessionManager = () => setShowSessionsPanel(false);
return (
<div className={`d-flex flex-column vh-100 ${theme.bodyBg}`}>
<ChatHeader
theme={theme}
onToggleTheme={onToggleTheme}
onReloadHistory={onReloadHistory}
onFreshStart={onFreshStart}
/>
<div className="flex-grow-1 overflow-auto">
<ChatWindow messages={messages} loading={loading} theme={theme} />
<>
{/* SHIFTING CONTENT */}
<div className={`sessions-wrapper d-flex flex-column vh-100 ${theme.bodyBg}`}>
<ChatHeader
theme={theme}
onToggleTheme={onToggleTheme}
onReloadHistory={onReloadHistory}
onFreshStart={onFreshStart}
onCreateSession={onCreateSession}
onManageSession={openSessionManager}
/>
<div className="flex-grow-1 overflow-auto">
<ChatWindow messages={messages} loading={loading} theme={theme} />
</div>
{loading && (
<div className="p-2 text-center">
<button className="btn btn-warning btn-sm" onClick={onStop}>
Stop Generating
</button>
</div>
)}
<ChatInput onSend={onSend} loading={loading} />
</div>
{loading && (
<div className="p-2 text-center">
<button className="btn btn-warning btn-sm" onClick={onStop}>
Stop Generating
</button>
</div>
)}
{/* DARK OVERLAY */}
<div
className={`sessions-overlay ${showSessionsPanel ? "show" : ""}`}
onClick={closeSessionManager}
></div>
<ChatInput onSend={onSend} loading={loading} />
</div>
{/* FIXED OFFCANVAS */}
<div
className={`offcanvas offcanvas-start ${showSessionsPanel ? "show" : ""}`}
tabIndex="-1"
style={{ visibility: showSessionsPanel ? "visible" : "hidden" }}
>
<div className="offcanvas-header">
<h5 className="offcanvas-title">Manage Sessions</h5>
<button
type="button"
className="btn-close text-reset"
onClick={closeSessionManager}
></button>
</div>
<div className="offcanvas-body">
<SessionTable
userId={userId}
onSelectSession={(sessionId) => {
onSelectSession(sessionId);
closeSessionManager();
}}
/>
</div>
</div>
</>
);
}
+79
View File
@@ -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 (
<div className={`d-flex flex-column vh-100 ${theme.bodyBg}`}>
<ChatHeader
theme={theme}
onToggleTheme={onToggleTheme}
onReloadHistory={onReloadHistory}
onFreshStart={onFreshStart}
onCreateSession={onCreateSession}
onManageSession={openSessionManager} // now opens the panel
/>
<div className="flex-grow-1 overflow-auto">
<ChatWindow messages={messages} loading={loading} theme={theme} />
</div>
{loading && (
<div className="p-2 text-center">
<button className="btn btn-warning btn-sm" onClick={onStop}>
⏹ Stop Generating
</button>
</div>
)}
<ChatInput onSend={onSend} loading={loading} />
{/* Offcanvas Session Manager */}
<div
className={`offcanvas offcanvas-start ${showSessionsPanel ? "show" : ""}`}
tabIndex="-1"
style={{ visibility: showSessionsPanel ? "visible" : "hidden" }}
>
<div className="offcanvas-header">
<h5 className="offcanvas-title">Manage Sessions</h5>
<button
type="button"
className="btn-close text-reset"
onClick={closeSessionManager}
></button>
</div>
<div className="offcanvas-body">
<SessionTable
userId={userId}
onSelectSession={(sessionId) => {
onSelectSession(sessionId); // tell parent to load history
closeSessionManager();
}}
/>
</div>
</div>
</div>
);
}
+138
View File
@@ -0,0 +1,138 @@
import React, { useEffect, useState } from "react";
export default function SessionTable({ userId, onSelectSession }) {
const [sessions, setSessions] = useState([]);
const [loading, setLoading] = useState(false);
const [editingSessionId, setEditingSessionId] = useState(null);
const [editName, setEditName] = useState("");
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 (
<div className="table-responsive">
<table className="table table-striped table-hover align-middle">
<thead>
<tr>
<th>Session Name</th>
<th>Created</th>
<th>Messages</th>
<th>Size (bytes)</th>
<th></th>
</tr>
</thead>
<tbody>
{loading && (
<tr>
<td colSpan="5" className="text-center">
Loading
</td>
</tr>
)}
{!loading && sessions.length === 0 && (
<tr>
<td colSpan="5" className="text-center">
No sessions found
</td>
</tr>
)}
{!loading &&
sessions.map((s) => (
<tr key={s.session_id}>
<td>
{editingSessionId === s.session_id ? (
<input
type="text"
className="form-control form-control-sm"
value={editName}
onChange={(e) => 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
/>
) : (
<span
role="button"
className="text-primary"
onClick={() => onSelectSession(s.session_id)}
title="Click to load this session"
>
{s.session_name}
</span>
)}
</td>
<td>{new Date(s.created_at).toLocaleString()}</td>
<td>{s.message_count}</td>
<td>{s.history_size_bytes}</td>
<td className="text-end">
<button
className="btn btn-sm btn-outline-secondary me-1"
onClick={() => startEditing(s)}
title="Rename"
>
</button>
<button
className="btn btn-sm btn-outline-danger"
onClick={() => deleteSession(s.session_id)}
title="Delete"
>
🗑
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}