diff --git a/frontend/public/emily-avatar-200.png b/frontend/public/emily-avatar-200.png new file mode 100644 index 0000000..3a2f811 Binary files /dev/null and b/frontend/public/emily-avatar-200.png differ diff --git a/frontend/public/emily-avatar-48.png b/frontend/public/emily-avatar-48.png new file mode 100644 index 0000000..090c963 Binary files /dev/null and b/frontend/public/emily-avatar-48.png differ diff --git a/frontend/public/emily-avatar-96.png b/frontend/public/emily-avatar-96.png new file mode 100644 index 0000000..3d5fd0b Binary files /dev/null and b/frontend/public/emily-avatar-96.png differ diff --git a/frontend/public/emily-avatar.png b/frontend/public/emily-avatar.png new file mode 100644 index 0000000..5be5c38 Binary files /dev/null and b/frontend/public/emily-avatar.png differ diff --git a/frontend/src/App.css b/frontend/src/App.css index 1bbf268..b587414 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -497,11 +497,21 @@ body { .message-header { display: flex; - flex-direction: column; + flex-direction: row; + align-items: center; + flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.5rem; } +.emily-avatar { + width: 28px; + height: 28px; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; +} + .agent-name { font-size: 0.75rem; color: var(--muted-foreground); @@ -534,7 +544,10 @@ body { } .loading-animation { - text-align: center; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; color: var(--muted-foreground); padding: 0.75rem; font-size: 0.875rem; @@ -904,6 +917,14 @@ body { margin-bottom: 1.5rem; } +.login-emily-avatar { + width: 96px; + height: 96px; + border-radius: 50%; + object-fit: cover; + margin-bottom: 0.5rem; +} + .login-card h1 { font-size: 2rem; color: #f8fafc; diff --git a/frontend/src/App.js b/frontend/src/App.js index 15b8a6e..e76489c 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -4,8 +4,9 @@ import "./App.css"; import { ThemeToggle } from "./components/ThemeToggle"; import { ResizableLayout } from "./components/ResizableLayout"; import { ReactComponent as EcoSeekLogo } from "./ecoseek-logo.svg"; +import emilyAvatar from "./emily-avatar.png"; import { useAuth } from "./contexts/AuthContext"; -import { chatCompletion, checkHealth, BROKER_URL, CHAT_URL, IS_LOCAL_EMILY } from "./api/broker"; +import { chatCompletion, checkHealth, checkRemoteHealth, BROKER_URL, CHAT_URL, IS_LOCAL_EMILY, HERMES_REMOTE_URL } from "./api/broker"; function LoginScreen({ onLogin }) { return ( @@ -13,6 +14,7 @@ function LoginScreen({ onLogin }) {

EcoSeek

+ Emily

Meet Emily — your AI ecological research assistant

@@ -39,6 +41,7 @@ function App() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [isOnline, setIsOnline] = useState(false); + const [remoteStatus, setRemoteStatus] = useState(null); const [expandedReasoning, setExpandedReasoning] = useState(new Set()); const messagesEndRef = useRef(null); @@ -49,14 +52,16 @@ function App() { } }, [handleCallback]); - // Health polling + // Health polling (local + remote) useEffect(() => { const poll = async () => { const ok = await checkHealth(); setIsOnline(ok); + const remote = await checkRemoteHealth(); + setRemoteStatus(remote); }; poll(); - const id = setInterval(poll, 10000); + const id = setInterval(poll, 15000); return () => clearInterval(id); }, []); @@ -237,7 +242,10 @@ function App() { >
{msg.type === "agent" && ( - Emily + <> + Emily + Emily + )} {msg.type === "agent" && msg.reasoning && ( <> @@ -271,7 +279,10 @@ function App() {
{isLoading && ( -
Emily is thinking...
+
+ Emily + Emily is thinking... +
)}
@@ -308,29 +319,59 @@ function App() {
-

Connection

-
- Auth - {BROKER_URL} -
+

Emily Local

- Chat + Endpoint {CHAT_URL}
- Mode - - {IS_LOCAL_EMILY ? "Emily Local + Hermes Remote" : "Emily Remote"} + Status + + {isOnline ? "Connected" : "Disconnected"}
+
+ +
+

Hermes Remote (reumanlab)

+
+ Endpoint + {HERMES_REMOTE_URL} +
Status - {isOnline ? "Connected" : "Disconnected"} + {remoteStatus ? "Connected" : "Disconnected"} + +
+ {remoteStatus && ( +
+ Platform + + {remoteStatus.platform || "hermes-agent"} + +
+ )} +
+ +
+

Auth

+
+ Broker + {BROKER_URL} +
+
+ Mode + + {IS_LOCAL_EMILY ? "Emily Local + Hermes Remote" : "Emily Remote"}
diff --git a/frontend/src/api/broker.js b/frontend/src/api/broker.js index e6d2e12..9e0f172 100644 --- a/frontend/src/api/broker.js +++ b/frontend/src/api/broker.js @@ -103,6 +103,19 @@ export async function checkHealth() { } } +const HERMES_REMOTE_URL = + process.env.REACT_APP_HERMES_REMOTE_URL || "https://hermes.ecoseek.org"; + +export async function checkRemoteHealth() { + try { + const res = await fetch(`${HERMES_REMOTE_URL}/health`); + if (!res.ok) return null; + return await res.json(); + } catch { + return null; + } +} + // ── Emily persona ────────────────────────────────────────────────────── const EMILY_SYSTEM_PROMPT = `You are Emily, an expert ecological scientist and AI assistant for EcoSeek. @@ -172,4 +185,4 @@ export async function chatCompletion(messages, model = "hermes") { return body; } -export { BROKER_URL, EMILY_URL, CHAT_URL, IS_LOCAL_EMILY }; +export { BROKER_URL, EMILY_URL, CHAT_URL, IS_LOCAL_EMILY, HERMES_REMOTE_URL }; diff --git a/frontend/src/emily-avatar.png b/frontend/src/emily-avatar.png new file mode 100644 index 0000000..3a2f811 Binary files /dev/null and b/frontend/src/emily-avatar.png differ