Merge pull request #303 from RecentRunner/chat-ui-updates

chat UI updates
This commit is contained in:
2026-04-15 01:37:52 -06:00
committed by GitHub
2 changed files with 766 additions and 278 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,6 @@ function ChatPage() {
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [conversations, setConversations] = useState([]); const [conversations, setConversations] = useState([]);
const [convsLoading, setConvsLoading] = useState(false); const [convsLoading, setConvsLoading] = useState(false);
const [showSidebar, setShowSidebar] = useState(false);
const [selectedFile, setSelectedFile] = useState(null); const [selectedFile, setSelectedFile] = useState(null);
const messagesEndRef = useRef(null); const messagesEndRef = useRef(null);
@@ -172,13 +171,17 @@ function ChatPage() {
} }
if (!convId) { if (!convId) {
await fetchConversations();
setLoadingConv(false); setLoadingConv(false);
setConversation(null); setConversation(null);
return; return;
} }
await fetchConversation(convId); await Promise.all([
await fetchMessages(convId); fetchConversation(convId),
fetchMessages(convId),
fetchConversations(),
]);
setLoadingConv(false); setLoadingConv(false);
startPolling(convId); startPolling(convId);
} }
@@ -188,11 +191,7 @@ function ChatPage() {
return () => { return () => {
if (pollRef.current) clearInterval(pollRef.current); if (pollRef.current) clearInterval(pollRef.current);
}; };
}, [token, authLoading, conversationIdParam, fetchConversation, fetchMessages, startPolling]); }, [token, authLoading, conversationIdParam, fetchConversation, fetchMessages, startPolling, fetchConversations]);
useEffect(() => {
if (showSidebar && token) fetchConversations();
}, [showSidebar, token, fetchConversations]);
async function handleSend(e) { async function handleSend(e) {
e?.preventDefault(); e?.preventDefault();
@@ -334,7 +333,7 @@ function ChatPage() {
}); });
setConversation(conv); setConversation(conv);
await fetchMessages(conv.id); await Promise.all([fetchMessages(conv.id), fetchConversations()]);
setLoadingConv(false); setLoadingConv(false);
startPolling(conv.id); startPolling(conv.id);
router.replace(`/chat?id=${conv.id}`, { scroll: false }); router.replace(`/chat?id=${conv.id}`, { scroll: false });
@@ -354,7 +353,26 @@ function ChatPage() {
setLoadingConv(false); setLoadingConv(false);
startPolling(convId); startPolling(convId);
router.replace(`/chat?id=${convId}`, { scroll: false }); router.replace(`/chat?id=${convId}`, { scroll: false });
setShowSidebar(false); }
async function handleCloseConversation() {
if (!conversation || conversation.status === "CLOSED") return;
try {
const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${conversation.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ status: "CLOSED" }),
});
if (!res.ok) return;
const updated = await res.json();
setConversation(updated);
await fetchConversations();
} catch {
// silent
}
} }
if (authLoading || loadingConv) { if (authLoading || loadingConv) {
@@ -392,11 +410,9 @@ function ChatPage() {
<section style={s.chatSection}> <section style={s.chatSection}>
<div style={{ display: "flex", gap: "1rem", alignItems: "flex-start" }}> <div style={{ display: "flex", gap: "1rem", alignItems: "flex-start" }}>
{showSidebar && (
<div style={s.sidebar}> <div style={s.sidebar}>
<div style={s.sidebarHeader}> <div style={s.sidebarHeader}>
<span style={s.sidebarTitle}>All Conversations</span> <span style={s.sidebarTitle}>All Conversations</span>
<button style={s.sidebarClose} onClick={() => setShowSidebar(false)}></button>
</div> </div>
{convsLoading && <p style={s.sidebarEmpty}>Loading...</p>} {convsLoading && <p style={s.sidebarEmpty}>Loading...</p>}
{!convsLoading && conversations.length === 0 && ( {!convsLoading && conversations.length === 0 && (
@@ -422,12 +438,11 @@ function ChatPage() {
</button> </button>
))} ))}
</div> </div>
<button style={s.newConvSidebarBtn} onClick={() => { setShowSidebar(false); handleNewConversation(); }}> <button style={s.newConvSidebarBtn} onClick={() => handleNewConversation()}>
+ New Conversation + New Conversation
</button> </button>
</div> </div>
)} <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ flex: 1, minWidth: 0 }}>
{!conversation ? ( {!conversation ? (
<div style={s.noConvCard}> <div style={s.noConvCard}>
<div style={s.noConvIcon}>💬</div> <div style={s.noConvIcon}>💬</div>
@@ -437,9 +452,6 @@ function ChatPage() {
<button style={s.startBtn} onClick={handleNewConversation}> <button style={s.startBtn} onClick={handleNewConversation}>
Start a Conversation Start a Conversation
</button> </button>
<button style={s.backBtn} onClick={() => setShowSidebar(true)}>
View Past Conversations
</button>
<button style={s.backBtn} onClick={() => router.push("/ai-chat")}> <button style={s.backBtn} onClick={() => router.push("/ai-chat")}>
Back to AI Assistant Back to AI Assistant
</button> </button>
@@ -460,12 +472,15 @@ function ChatPage() {
</div> </div>
</div> </div>
<div style={{ display: "flex", gap: "0.5rem" }}> <div style={{ display: "flex", gap: "0.5rem" }}>
<button {!isClosed && (
style={{ ...s.historyBtn, ...(showSidebar ? s.historyBtnActive : {}) }} <button
onClick={() => setShowSidebar((v) => !v)} style={s.closeConvBtn}
> onClick={handleCloseConversation}
History title="Close this conversation"
</button> >
Close Chat
</button>
)}
<button <button
style={s.aiBtn} style={s.aiBtn}
onClick={() => router.push("/ai-chat")} onClick={() => router.push("/ai-chat")}
@@ -992,10 +1007,10 @@ const s = {
padding: "0 0.15rem", padding: "0 0.15rem",
flexShrink: 0, flexShrink: 0,
}, },
historyBtn: { closeConvBtn: {
background: "white", background: "white",
border: "2px solid #555", border: "2px solid #c0392b",
color: "#555", color: "#c0392b",
borderRadius: 8, borderRadius: 8,
padding: "0.45rem 0.9rem", padding: "0.45rem 0.9rem",
fontSize: "0.82rem", fontSize: "0.82rem",
@@ -1003,10 +1018,6 @@ const s = {
cursor: "pointer", cursor: "pointer",
whiteSpace: "nowrap", whiteSpace: "nowrap",
}, },
historyBtnActive: {
background: "#555",
color: "white",
},
sidebar: { sidebar: {
width: 230, width: 230,
flexShrink: 0, flexShrink: 0,
@@ -1028,14 +1039,6 @@ const s = {
flexShrink: 0, flexShrink: 0,
}, },
sidebarTitle: { fontWeight: 700, fontSize: "0.88rem", color: "#333" }, sidebarTitle: { fontWeight: 700, fontSize: "0.88rem", color: "#333" },
sidebarClose: {
background: "none",
border: "none",
cursor: "pointer",
fontSize: "0.85rem",
color: "#999",
padding: "0.1rem 0.25rem",
},
sidebarEmpty: { sidebarEmpty: {
color: "#aaa", color: "#aaa",
fontSize: "0.82rem", fontSize: "0.82rem",