diff --git a/frontend/src/App.css b/frontend/src/App.css index 1f0ac49..db6eebf 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -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; } + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 9dcd044..9541539 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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 ( ); } diff --git a/frontend/src/ChatHeader.jsx b/frontend/src/ChatHeader.jsx index 5b2b22e..d723524 100644 --- a/frontend/src/ChatHeader.jsx +++ b/frontend/src/ChatHeader.jsx @@ -5,45 +5,65 @@ export default function ChatHeader({ theme, onToggleTheme, onReloadHistory, - onFreshStart + onFreshStart, + onCreateSession, + onManageSession }) { return (
- {/* Left column: new buttons */} -
+ {/* Left column: control buttons */} +
+ + + + +
{/* Center column: title */} -
+

πŸ€– EgalWare's LLM ChatBot

{/* Right column: theme toggle */}
); } - diff --git a/frontend/src/ChatLayout.jsx b/frontend/src/ChatLayout.jsx index b51f44b..4edc3f3 100644 --- a/frontend/src/ChatLayout.jsx +++ b/frontend/src/ChatLayout.jsx @@ -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 ( -
- -
- + <> + {/* SHIFTING CONTENT */} +
+ + +
+ +
+ + {loading && ( +
+ +
+ )} + +
- {loading && ( -
- -
- )} + {/* DARK OVERLAY */} +
- -
+ {/* FIXED OFFCANVAS */} +
+
+
Manage Sessions
+ +
+
+ { + onSelectSession(sessionId); + closeSessionManager(); + }} + /> +
+
+ ); } + 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..b57b07a --- /dev/null +++ b/frontend/src/SessionTable.jsx @@ -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 ( +
+ + + + + + + + + + + + {loading && ( + + + + )} + {!loading && sessions.length === 0 && ( + + + + )} + {!loading && + sessions.map((s) => ( + + + + + + + + ))} + +
Session NameCreatedMessagesSize (bytes)
+ 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)} + title="Click to load this session" + > + {s.session_name} + + )} + {new Date(s.created_at).toLocaleString()}{s.message_count}{s.history_size_bytes} + + +
+
+ ); +} +