Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions frontend/chat-ui/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -25,7 +24,6 @@ function AppInner() {
<ChatWindow />
<InputBox />
</div>
<ProfileMenu />
</div>
);
}
Expand Down
17 changes: 12 additions & 5 deletions frontend/chat-ui/src/components/ModeSelectorDemo.jsx
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand Down Expand Up @@ -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",
},
});
Expand Down
92 changes: 72 additions & 20 deletions frontend/chat-ui/src/components/ProfileMenu.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={`fixed bottom-2 z-10 transition-all ${state.sidebarOpen ? 'left-64' : 'left-2'}`}
>
<button
className="p-2 rounded-full"
onClick={() => setOpen((o) => !o)}
title="Profile"
aria-expanded={open}
aria-controls="profile-menu"
<div ref={containerRef} className={`relative ${className}`}>
<button
type="button"
className="flex h-10 w-10 items-center justify-center rounded-full border border-gray-200 bg-white text-lg shadow-sm transition hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-900 dark:hover:bg-gray-800"
onClick={() => setOpen((o) => !o)}
aria-haspopup="menu"
aria-expanded={open}
aria-controls={menuId}
aria-label="Profile and settings"
title="Profile and settings"
>
⚙️
</button>
{open && (
<div
id="profile-menu"
className="mt-2 p-2 border rounded bg-white dark:bg-gray-800 shadow space-y-2"
id={menuId}
role="menu"
className="absolute right-0 top-full z-20 mt-2 w-56 rounded-xl border border-gray-200 bg-white p-2 text-sm shadow-lg dark:border-gray-700 dark:bg-gray-800"
>
<button className="block w-full text-left" onClick={toggleTheme}>
Toggle Theme
<button
type="button"
className="flex w-full items-center justify-between rounded-lg px-3 py-2 text-left transition hover:bg-gray-100 dark:hover:bg-gray-700"
onClick={toggleTheme}
>
<span>Theme</span>
<span className="text-xs text-gray-500 dark:text-gray-300">{state.theme === 'light' ? 'Light' : 'Dark'}</span>
</button>
<button
type="button"
className="flex w-full items-center justify-between rounded-lg px-3 py-2 text-left transition hover:bg-gray-100 dark:hover:bg-gray-700"
onClick={toggleMemory}
>
<span>Memory</span>
<span className="text-xs text-gray-500 dark:text-gray-300">{state.memory ? 'On' : 'Off'}</span>
</button>
<hr className="my-1 border-gray-200 dark:border-gray-700" />
<button
type="button"
className="block w-full rounded-lg px-3 py-2 text-left transition hover:bg-gray-100 dark:hover:bg-gray-700"
>
Custom instructions
</button>
<button className="block w-full text-left" onClick={toggleMemory}>
Memory: {state.memory ? 'On' : 'Off'}
<button
type="button"
className="block w-full rounded-lg px-3 py-2 text-left transition hover:bg-gray-100 dark:hover:bg-gray-700"
>
Account
</button>
<button className="block w-full text-left">Custom instructions</button>
<button className="block w-full text-left">Account</button>
</div>
)}
</div>
Expand Down
52 changes: 35 additions & 17 deletions frontend/chat-ui/src/components/TopBar.jsx
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -44,33 +45,50 @@ export default function TopBar() {
};

return (
<div className="relative flex items-center justify-between p-2 border-b">
<div className="flex items-center gap-2 border-b p-2">
<input
className="text-lg font-semibold flex-1 mr-2 bg-transparent"
className="flex-1 min-w-0 bg-transparent text-lg font-semibold focus:outline-none"
value={title}
onChange={(e) => rename(e.target.value)}
placeholder="Untitled conversation"
/>
<select value={model} onChange={(e) => setModel(e.target.value)} className="border rounded p-1">
<select value={model} onChange={(e) => setModel(e.target.value)} className="rounded border px-2 py-1">
<option>Agrinet</option>
<option>Agrinet-2</option>
<option>Agrinet-3</option>
</select>
<button className="ml-2 p-2" onClick={() => dispatch({ type: 'TOGGLE_SIDEBAR' })}>
<button
type="button"
className="rounded border px-2 py-1 hover:bg-gray-100"
onClick={() => dispatch({ type: 'TOGGLE_SIDEBAR' })}
title="Toggle sidebar"
>
</button>
<button className="ml-2 p-2" onClick={() => setMenuOpen((o) => !o)}>
</button>
{menuOpen && (
<div className="absolute right-0 top-full mt-1 border rounded bg-white shadow">
<button
className="block px-4 py-2 text-left w-full hover:bg-gray-100"
onClick={remove}
>
Delete conversation
</button>
</div>
)}
<div className="relative">
<button
type="button"
className="rounded border px-2 py-1 hover:bg-gray-100"
onClick={() => setMenuOpen((o) => !o)}
aria-haspopup="menu"
aria-expanded={menuOpen}
title="Conversation options"
>
</button>
{menuOpen && (
<div className="absolute right-0 top-full z-10 mt-1 w-48 rounded border bg-white shadow-lg">
<button
type="button"
className="block w-full px-4 py-2 text-left text-sm hover:bg-gray-100"
onClick={remove}
>
Delete conversation
</button>
</div>
)}
</div>
<ProfileMenu className="ml-1" />
</div>
);
}