diff --git a/app/frontend/src/App.jsx b/app/frontend/src/App.jsx
index d354dc9..8c2fa2b 100644
--- a/app/frontend/src/App.jsx
+++ b/app/frontend/src/App.jsx
@@ -133,6 +133,58 @@ function App() {
localStorage.setItem("textSettings", JSON.stringify(textSettings));
}, [textSettings]);
+ // Lifted Chat History States
+ const [conversations, setConversations] = useState([]);
+ const [activeConversationId, setActiveConversationId] = useState(null);
+ const [showHistory, setShowHistory] = useState(false); // Default hide
+
+ useEffect(() => {
+ const saved = localStorage.getItem("chat_conversations");
+ if (saved) {
+ try {
+ setConversations(JSON.parse(saved));
+ } catch (_) {}
+ }
+ }, []);
+
+ const saveConversationState = useCallback((id, msgs, modelName, newTitle = null) => {
+ setConversations((prev) => {
+ const list = [...prev];
+ const idx = list.findIndex(c => c.id === id);
+ if (idx !== -1) {
+ list[idx] = {
+ ...list[idx],
+ messages: msgs,
+ timestamp: Date.now(),
+ model: modelName,
+ ...(newTitle ? { title: newTitle } : {})
+ };
+ } else {
+ list.unshift({
+ id,
+ title: newTitle || "Chat Session",
+ model: modelName,
+ messages: msgs,
+ timestamp: Date.now()
+ });
+ }
+ localStorage.setItem("chat_conversations", JSON.stringify(list));
+ return list;
+ });
+ }, []);
+
+ const handleDeleteConversation = useCallback((id, e) => {
+ if (e) e.stopPropagation();
+ setConversations((prev) => {
+ const filtered = prev.filter((c) => c.id !== id);
+ localStorage.setItem("chat_conversations", JSON.stringify(filtered));
+ return filtered;
+ });
+ if (activeConversationId === id) {
+ setActiveConversationId(null);
+ }
+ }, [activeConversationId]);
+
// Load hardware specifications on mount
useEffect(() => {
async function loadSpecs() {
@@ -371,8 +423,14 @@ function App() {
activeTab={activeTab}
setActiveTab={setActiveTab}
specs={specs}
+ conversations={conversations}
+ activeConversationId={activeConversationId}
+ setActiveConversationId={setActiveConversationId}
+ showHistory={showHistory}
+ setShowHistory={setShowHistory}
+ onDeleteConversation={handleDeleteConversation}
/>
- ), [activeTab, specs]);
+ ), [activeTab, specs, conversations, activeConversationId, showHistory, handleDeleteConversation]);
const handleStopServer = useCallback(async () => {
if (!serverRunning || isStoppingServer) return;
@@ -513,6 +571,13 @@ function App() {
setTextSettings={setTextSettings}
setActiveModel={setActiveModel}
setServerRunning={setServerRunning}
+ conversations={conversations}
+ setConversations={setConversations}
+ activeConversationId={activeConversationId}
+ setActiveConversationId={setActiveConversationId}
+ showHistory={showHistory}
+ setShowHistory={setShowHistory}
+ saveConversationState={saveConversationState}
/>
diff --git a/app/frontend/src/components/Sidebar.jsx b/app/frontend/src/components/Sidebar.jsx
index b9249ff..db86544 100644
--- a/app/frontend/src/components/Sidebar.jsx
+++ b/app/frontend/src/components/Sidebar.jsx
@@ -1,7 +1,17 @@
import React, { memo } from "react";
-import { Image, FolderDown, MessageSquare, Settings, Shield, Terminal } from "lucide-react";
+import { Image, FolderDown, MessageSquare, Settings, Shield, Terminal, ChevronDown, ChevronUp, Trash2 } from "lucide-react";
-function Sidebar({ activeTab, setActiveTab, specs }) {
+function Sidebar({
+ activeTab,
+ setActiveTab,
+ specs,
+ conversations = [],
+ activeConversationId,
+ setActiveConversationId,
+ showHistory,
+ setShowHistory,
+ onDeleteConversation
+}) {
return (
@@ -21,12 +31,121 @@ function Sidebar({ activeTab, setActiveTab, specs }) {
Image Generator
-
setActiveTab("chat")}
- >
-
-
Text Chat
+
+
setActiveTab("chat")}
+ style={{ display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%", boxSizing: "border-box" }}
+ >
+
+
+ Text Chat
+
+
+
+
+ {/* Sidebar Chat History List */}
+ {showHistory && (
+
+ {conversations.length === 0 ? (
+
+ No saved chats
+
+ ) : (
+ conversations.map((conv) => {
+ const isActive = activeConversationId === conv.id;
+ return (
+
{
+ setActiveConversationId(conv.id);
+ setActiveTab("chat");
+ }}
+ style={{
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+ padding: "6px 8px 6px 10px",
+ borderRadius: "var(--md-shape-corner-small)",
+ fontSize: "0.78rem",
+ cursor: "pointer",
+ background: isActive ? "var(--md-sys-color-secondary-container)" : "transparent",
+ color: isActive ? "var(--md-sys-color-on-secondary-container)" : "var(--md-sys-color-on-surface-variant)",
+ border: isActive ? "1px solid var(--md-sys-color-outline-variant)" : "1px solid transparent",
+ transition: "background 0.2s"
+ }}
+ className="sidebar-history-item"
+ title={conv.title}
+ >
+
+ {conv.title}
+
+
+
+ );
+ })
+ )}
+
+ )}
{
- const saved = localStorage.getItem("chat_conversations");
- if (saved) {
- try {
- setConversations(JSON.parse(saved));
- } catch (_) {}
+ if (activeConversationId) {
+ const conv = conversations.find(c => c.id === activeConversationId);
+ if (conv) {
+ setMessages(conv.messages);
+ if (conv.model && models.some(m => m.filename === conv.model)) {
+ setSelectedModel(conv.model);
+ }
+ const total = conv.messages.reduce((sum, m) => {
+ const text = Array.isArray(m.content)
+ ? m.content.map(c => c.text || "").join(" ")
+ : (m.content || "");
+ return sum + text.split(/\s+/).length;
+ }, 0);
+ setTokenUsage({
+ prompt_tokens: Math.round(total * 0.7),
+ completion_tokens: Math.round(total * 0.3),
+ total_tokens: total
+ });
+ }
+ } else {
+ setMessages([]);
+ setTokenUsage({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
}
- }, []);
+ }, [activeConversationId, conversations, models]);
const refresh = useCallback(async () => {
const [nextModels, nextStatus] = await Promise.all([listLlmModels(), getLlmStatus()]);
@@ -197,72 +226,8 @@ function TextChat({ specs, showAlert, showConfirm, textSettings, setTextSettings
setSelectedModel("");
};
- const saveConversationState = (id, msgs, modelName, newTitle = null) => {
- const saved = localStorage.getItem("chat_conversations");
- let list = [];
- if (saved) {
- try {
- list = JSON.parse(saved);
- } catch (_) {}
- }
- const idx = list.findIndex(c => c.id === id);
- if (idx !== -1) {
- list[idx].messages = msgs;
- list[idx].timestamp = Date.now();
- list[idx].model = modelName;
- if (newTitle) list[idx].title = newTitle;
- } else {
- list.unshift({
- id,
- title: newTitle || "Chat Session",
- model: modelName,
- messages: msgs,
- timestamp: Date.now()
- });
- }
- localStorage.setItem("chat_conversations", JSON.stringify(list));
- setConversations(list);
- };
-
const handleNewChat = () => {
setActiveConversationId(null);
- setMessages([]);
- setTokenUsage({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
- };
-
- const handleSelectConversation = (conv) => {
- setActiveConversationId(conv.id);
- setMessages(conv.messages);
- if (conv.model && models.some(m => m.filename === conv.model)) {
- setSelectedModel(conv.model);
- }
- // Try to compute approximate token usage from messages
- const total = conv.messages.reduce((sum, m) => {
- const text = Array.isArray(m.content)
- ? m.content.map(c => c.text || "").join(" ")
- : (m.content || "");
- return sum + text.split(/\s+/).length;
- }, 0);
- setTokenUsage({
- prompt_tokens: Math.round(total * 0.7),
- completion_tokens: Math.round(total * 0.3),
- total_tokens: total
- });
- };
-
- const handleDeleteConversation = (id, e) => {
- e.stopPropagation();
- const saved = localStorage.getItem("chat_conversations");
- if (!saved) return;
- try {
- const list = JSON.parse(saved);
- const filtered = list.filter(c => c.id !== id);
- localStorage.setItem("chat_conversations", JSON.stringify(filtered));
- setConversations(filtered);
- if (activeConversationId === id) {
- handleNewChat();
- }
- } catch (_) {}
};
const sendMessage = async () => {
@@ -351,70 +316,7 @@ function TextChat({ specs, showAlert, showConfirm, textSettings, setTextSettings
};
return (
-
- {/* Collapsible Chat History Sidebar */}
- {showHistory && (
-
- )}
-
+