diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 9c716cf..da87586 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,177 +1,124 @@
-// 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';
+import React, { useState } from "react";
+import "./App.css";
+import { themes } from "./themes";
+import ChatWindow from "./ChatWindow";
+import ChatInput from "./ChatInput";
-
-function App() {
+export default 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 () => {
+ const [themeName, setThemeName] = useState("light");
+
+ const theme = themes[themeName];
+
+ const sendMessage = async (input) => {
if (!input.trim() || loading) return;
const userMessage = { role: "user", content: input };
- const userId = "user1";
+ const userId = "userTest";
+ const assistantIndex = messages.length + 1;
- // 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("");
+ setMessages((prev) => [
+ ...prev,
+ userMessage,
+ { role: "assistant", content: "" },
+ ]);
setLoading(true);
- reset(); // clear the buffer for the new response
try {
- const res = await fetch("/chat-stream", {
+ 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 })
+ 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;
- }
+ if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
const reader = res.body.getReader();
const decoder = new TextDecoder("utf-8");
- let acc = ""; // final committed text
- let sseBuffer = ""; // raw SSE buffer
+ let buffer = "";
+ let acc = "";
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 => {
+ buffer += decoder.decode(value, { stream: true });
+ const parts = buffer.split("\n\n");
+ buffer = parts.pop() || "";
+ for (const part of parts) {
+ const line = part
+ .split("\n")
+ .map((l) => l.trim())
+ .find((l) => l.startsWith("data:"));
+ if (!line) continue;
+ const data = line.slice(5).trim();
+ if (data === "[DONE]") {
+ 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;
+ 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
}
+ } catch {
+ // ignore malformed chunk
}
}
}
-
- // Stream ended without an explicit [DONE]
- setMessages(prev => {
- const next = [...prev];
- next[assistantIndex] = { role: "assistant", content: acc };
- return next;
- });
} catch (err) {
- setMessages(prev => {
+ setMessages((prev) => {
const next = [...prev];
- next[assistantIndex] = { role: "assistant", content: `Error: ${String(err)}` };
+ next[assistantIndex] = {
+ role: "assistant",
+ content: `Error: ${String(err)}`,
+ };
return next;
});
} finally {
setLoading(false);
}
};
-
- useEffect(() => {
- if (loading) {
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
- }
- }, [buffered, loading]);
+
+ const toggleTheme = () => {
+ setThemeName((t) => (t === "light" ? "dark" : "light"));
+ };
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/App.jsx.orig b/frontend/src/App.jsx.orig
new file mode 100644
index 0000000..d3d874b
--- /dev/null
+++ b/frontend/src/App.jsx.orig
@@ -0,0 +1,177 @@
+// 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 (
+
+
+
+
+ {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/AssistantMessage.jsx b/frontend/src/AssistantMessage.jsx
new file mode 100644
index 0000000..f1792b8
--- /dev/null
+++ b/frontend/src/AssistantMessage.jsx
@@ -0,0 +1,31 @@
+import React from "react";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import remarkMath from "remark-math";
+import rehypeKatex from "rehype-katex";
+
+export default function AssistantMessage({ content, theme }) {
+ return (
+
+
+
(
+
+ ),
+ th: (props) => |
+ }}
+ >
+ {content}
+
+
+
+ );
+}
+
+
diff --git a/frontend/src/ChatInput.jsx b/frontend/src/ChatInput.jsx
new file mode 100644
index 0000000..060fcf4
--- /dev/null
+++ b/frontend/src/ChatInput.jsx
@@ -0,0 +1,31 @@
+import React, { useState } from "react";
+
+export default function ChatInput({ onSend, loading }) {
+ const [value, setValue] = useState("");
+
+ const handleKeyDown = (e) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ if (value.trim()) {
+ onSend(value);
+ setValue("");
+ }
+ }
+ };
+
+ return (
+
+
+ );
+}
+
+
diff --git a/frontend/src/ChatWindow.jsx b/frontend/src/ChatWindow.jsx
new file mode 100644
index 0000000..c36287a
--- /dev/null
+++ b/frontend/src/ChatWindow.jsx
@@ -0,0 +1,31 @@
+import React, { useRef, useEffect } from "react";
+import UserMessage from "./UserMessage";
+import AssistantMessage from "./AssistantMessage";
+
+export default function ChatWindow({ messages, loading, theme }) {
+ const endRef = useRef(null);
+
+ useEffect(() => {
+ endRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, [messages]);
+
+ return (
+
+ {messages.map((m, idx) =>
+ m.role === "user" ? (
+
+ ) : (
+
+ )
+ )}
+ {loading && (
+
+ The model is processing...
+
+ )}
+
+
+ );
+}
+
+
diff --git a/frontend/src/UserMessage.jsx b/frontend/src/UserMessage.jsx
new file mode 100644
index 0000000..67fcbf4
--- /dev/null
+++ b/frontend/src/UserMessage.jsx
@@ -0,0 +1,21 @@
+import React from "react";
+
+export default function UserMessage({ content, theme }) {
+ return (
+
+ );
+}
+
+
diff --git a/frontend/src/themes.js b/frontend/src/themes.js
new file mode 100644
index 0000000..0bd113f
--- /dev/null
+++ b/frontend/src/themes.js
@@ -0,0 +1,17 @@
+// themes.js
+export const themes = {
+ light: {
+ userBg: "bg-primary text-white",
+ assistantBg: "bg-white border text-dark",
+ bodyBg: "bg-light",
+ headerBg: "bg-primary text-white",
+ },
+ dark: {
+ userBg: "bg-dark text-white",
+ assistantBg: "bg-secondary text-white",
+ bodyBg: "bg-black text-white",
+ headerBg: "bg-dark text-white",
+ }
+};
+
+