diff --git a/frontend/chat-ui/src/App.jsx b/frontend/chat-ui/src/App.jsx index 61fb96b..c36eadf 100644 --- a/frontend/chat-ui/src/App.jsx +++ b/frontend/chat-ui/src/App.jsx @@ -5,7 +5,6 @@ import InputBox from './components/InputBox.jsx'; import Sidebar from './components/Sidebar.jsx'; import TopBar from './components/TopBar.jsx'; import TestApi from './components/TestApi.jsx'; -import ProfileMenu from './components/ProfileMenu.jsx'; function AppInner() { const { state } = useStore(); @@ -25,7 +24,6 @@ function AppInner() { - ); } diff --git a/frontend/chat-ui/src/components/ModeSelectorDemo.jsx b/frontend/chat-ui/src/components/ModeSelectorDemo.jsx index 3be190b..decd3bf 100644 --- a/frontend/chat-ui/src/components/ModeSelectorDemo.jsx +++ b/frontend/chat-ui/src/components/ModeSelectorDemo.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useId, useMemo, useReducer, useRef, useState } from "react"; +import React, { useEffect, useId, useReducer, useRef, useState } from "react"; import api, { API_BASE_URL } from "../api"; /** @@ -585,18 +585,25 @@ function ChatComposer() { dispatch({ type: "SET_RESULTS", results: { text: `You said: ${q}\n\n(Connect your backend to replace this mock.)` } }); } else if (state.mode === "deep") { dispatch({ type: "DEEP_START" }); - try { + try { const res = await deepResearch(q, (stage, log) => { dispatch({ type: "DEEP_SET_STAGE", stage, log }); }); dispatch({ type: "SET_RESULTS", results: { text: res.text, citations: res.citations } }); - } catch (err) { + } catch (error) { + console.error("Deep research request failed", error); + const description = + error instanceof Error && error.message + ? error.message + : typeof error === "string" && error.length + ? error + : "Try again in a moment."; dispatch({ type: "ADD_TOAST", toast: { id: uid("toast"), - title: "Research failed", - desc: err.message ?? String(err), + title: "Deep research failed", + desc: description, kind: "error", }, }); diff --git a/frontend/chat-ui/src/components/ProfileMenu.jsx b/frontend/chat-ui/src/components/ProfileMenu.jsx index e5f3654..41ab45f 100644 --- a/frontend/chat-ui/src/components/ProfileMenu.jsx +++ b/frontend/chat-ui/src/components/ProfileMenu.jsx @@ -1,39 +1,91 @@ -import React, { useState } from 'react'; +import React, { useEffect, useId, useRef, useState } from 'react'; import { useStore } from '../store'; -export default function ProfileMenu() { +/** + * Compact profile/settings menu that can be embedded in any toolbar. + * Positioned relative to its container, so callers can control placement + * with layout utilities (e.g. flex gap, margin). + */ +export default function ProfileMenu({ className = '' }) { const { state, dispatch } = useStore(); const [open, setOpen] = useState(false); + const containerRef = useRef(null); + const menuId = useId(); - const toggleTheme = () => dispatch({ type: 'SET_THEME', theme: state.theme === 'light' ? 'dark' : 'light' }); + useEffect(() => { + if (!open) return undefined; + + const handleClick = (event) => { + if (containerRef.current && !containerRef.current.contains(event.target)) { + setOpen(false); + } + }; + + const handleKey = (event) => { + if (event.key === 'Escape') setOpen(false); + }; + + document.addEventListener('mousedown', handleClick); + document.addEventListener('keydown', handleKey); + return () => { + document.removeEventListener('mousedown', handleClick); + document.removeEventListener('keydown', handleKey); + }; + }, [open]); + + const toggleTheme = () => + dispatch({ type: 'SET_THEME', theme: state.theme === 'light' ? 'dark' : 'light' }); const toggleMemory = () => dispatch({ type: 'TOGGLE_MEMORY' }); return ( -
- {open && ( )}
diff --git a/frontend/chat-ui/src/components/TopBar.jsx b/frontend/chat-ui/src/components/TopBar.jsx index f7968b3..66f165a 100644 --- a/frontend/chat-ui/src/components/TopBar.jsx +++ b/frontend/chat-ui/src/components/TopBar.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useStore } from '../store'; import { put, del } from '../api'; +import ProfileMenu from './ProfileMenu.jsx'; export default function TopBar() { const { state, dispatch } = useStore(); @@ -44,33 +45,50 @@ export default function TopBar() { }; return ( -
+
rename(e.target.value)} + placeholder="Untitled conversation" /> - setModel(e.target.value)} className="rounded border px-2 py-1"> - - - {menuOpen && ( -
- -
- )} +
+ + {menuOpen && ( +
+ +
+ )} +
+
); }