diff --git a/packages/ema-ui/package.json b/packages/ema-ui/package.json index 7c9ed01d..8c0216c0 100644 --- a/packages/ema-ui/package.json +++ b/packages/ema-ui/package.json @@ -12,6 +12,7 @@ "@lancedb/lancedb": "^0.23.0", "arktype": "^2.1.29", "ema": "workspace:*", + "mongodb-agenda": "npm:mongodb@4.17.2", "mongodb": "^7.0.0", "next": "16.0.10", "pino": "^10.1.0", diff --git a/packages/ema-ui/src/app/chat/page.module.css b/packages/ema-ui/src/app/chat/page.module.css index ce1ab310..01101495 100644 --- a/packages/ema-ui/src/app/chat/page.module.css +++ b/packages/ema-ui/src/app/chat/page.module.css @@ -44,26 +44,27 @@ } .snapshotStatus { - max-width: 800px; - margin: -1rem auto 1rem; + position: fixed; + left: 50%; + top: 1rem; + transform: translateX(-50%); + max-width: 720px; + width: calc(100% - 2rem); + margin: 0; padding: 0.75rem 1rem; border-radius: 12px; - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(255, 255, 255, 0.12); - color: #cdd6f7; + background: #cdd6f7; + border: 1px solid rgba(0, 0, 0, 0.12); + color: #30354a; font-size: 0.9rem; - animation: toastFade 3s ease forwards; + text-align: center; + animation: toastFade 2.8s ease forwards; + z-index: 30; + pointer-events: none; } @keyframes toastFade { - 0% { - opacity: 0; - transform: translateY(-4px); - } - 15% { - opacity: 1; - transform: translateY(0); - } + 0%, 70% { opacity: 1; } diff --git a/packages/ema-ui/src/app/chat/page.tsx b/packages/ema-ui/src/app/chat/page.tsx index 08ec477a..ff0346df 100644 --- a/packages/ema-ui/src/app/chat/page.tsx +++ b/packages/ema-ui/src/app/chat/page.tsx @@ -4,12 +4,16 @@ import { useState, useEffect, useRef } from "react"; import styles from "./page.module.css"; import type { ActorAgentEvent, Message } from "ema"; +let initialLoadPromise: Promise | null = null; +let initialMessagesCache: Message[] | null = null; + // todo: consider adding tests for this component to verify message state management export default function ChatPage() { const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(""); + const [initializing, setInitializing] = useState(true); const [snapshotting, setSnapshotting] = useState(false); - const [snapshotStatus, setSnapshotStatus] = useState(null); + const [notice, setNotice] = useState(null); const chatAreaRef = useRef(null); const messagesEndRef = useRef(null); const shouldAutoScrollRef = useRef(true); @@ -21,24 +25,48 @@ export default function ChatPage() { let isActive = true; const init = async () => { + setInitializing(true); try { - await fetch("/api/users/login"); - } catch (error) { - console.error("Error logging in:", error); - } + if (!initialLoadPromise) { + initialLoadPromise = (async () => { + try { + await fetch("/api/users/login"); + } catch (error) { + console.error("Error logging in:", error); + } - try { - const response = await fetch( - "/api/conversations/messages?conversationId=1&limit=100", - ); - if (response.ok) { - const data = (await response.json()) as { messages: Message[] }; - if (isActive && Array.isArray(data.messages)) { - setMessages(data.messages); - } + try { + const response = await fetch( + "/api/conversations/messages?conversationId=1&limit=100", + ); + if (response.ok) { + const data = (await response.json()) as { messages: Message[] }; + if (Array.isArray(data.messages)) { + return data.messages; + } + } + } catch (error) { + console.error("Error loading history:", error); + } + return null; + })().catch((error) => { + initialLoadPromise = null; + throw error; + }); + } + + if (!initialMessagesCache) { + initialMessagesCache = await initialLoadPromise; + } + + if (isActive && Array.isArray(initialMessagesCache)) { + setMessages(initialMessagesCache); + setNotice("Conversation history loaded."); + } + } finally { + if (isActive) { + setInitializing(false); } - } catch (error) { - console.error("Error loading history:", error); } if (!isActive) { @@ -101,14 +129,14 @@ export default function ChatPage() { }, [messages.length]); useEffect(() => { - if (!snapshotStatus) { + if (!notice) { return; } if (snapshotTimerRef.current) { clearTimeout(snapshotTimerRef.current); } snapshotTimerRef.current = setTimeout(() => { - setSnapshotStatus(null); + setNotice(null); snapshotTimerRef.current = null; }, 3200); return () => { @@ -117,7 +145,7 @@ export default function ChatPage() { snapshotTimerRef.current = null; } }; - }, [snapshotStatus]); + }, [notice]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -172,7 +200,7 @@ export default function ChatPage() { }; const handleSnapshot = async () => { - setSnapshotStatus(null); + setNotice(null); setSnapshotting(true); try { const response = await fetch("/api/snapshot", { @@ -182,20 +210,20 @@ export default function ChatPage() { }); if (!response.ok) { const text = await response.text(); - setSnapshotStatus(text || "Snapshot failed."); + setNotice(text || "Snapshot failed."); return; } const data = (await response.json().catch(() => null)) as { fileName?: string; } | null; - setSnapshotStatus( + setNotice( data?.fileName ? `Snapshot saved: ${data.fileName}` : "Snapshot created.", ); } catch (error) { console.error("Snapshot error:", error); - setSnapshotStatus("Snapshot failed."); + setNotice("Snapshot failed."); } finally { setSnapshotting(false); } @@ -215,9 +243,7 @@ export default function ChatPage() { {snapshotting ? "Snapshotting..." : "Snapshot"} - {snapshotStatus ? ( -
{snapshotStatus}
- ) : null} + {notice ?
{notice}
: null}
setInputValue(e.target.value)} + disabled={initializing} />