diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 9541539..c8c2589 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4,12 +4,13 @@ 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(); const [themeName, setThemeName] = useState("light"); const theme = themes[themeName]; + const sessionId = getSessionId(); const userId = getUserId(); @@ -29,16 +30,14 @@ export default function App() { const reloadHistory = async () => { const res = await fetch(`/v1/history?user_id=${userId}&session_id=${sessionId}`); 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 () => { @@ -50,7 +49,7 @@ export default function App() { }); const meta = await res.json(); setSessionId(meta.session_id); - setMessages([]); // clear chat window + setMessages([]); }; const editSession = async () => { @@ -74,8 +73,9 @@ export default function App() { onReloadHistory={reloadHistory} onFreshStart={freshStart} onCreateSession={createSession} + onEditSession={editSession} + 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 d723524..3537950 100644 --- a/frontend/src/ChatHeader.jsx +++ b/frontend/src/ChatHeader.jsx @@ -14,7 +14,7 @@ export default function ChatHeader({ className={`${theme.headerBg} p-2 sticky-top shadow row align-items-center`} > {/* Left column: control buttons */} -
+