feat(memories): redesign MemoryDetailsHeader UI and update tests (#13263)

* feat(memory): improve memory UI with empty states, toasts, and model filtering

* [autofix.ci] apply automated fixes

* fix: capture memory id/name before setTimeout to prevent stale closure null access

* error improvement and testcase addition

* fix: surface refresh errors, restore API error messages, flatten sidebar ternary, and add keyboard access to batch-size tooltip

* [autofix.ci] apply automated fixes

* biome fix

* fix types

* [autofix.ci] apply automated fixes

* fix: UI polish for Memories and Traces sidebar sections

* [autofix.ci] apply automated fixes

* feat(memories): surface memory config in summary card with popover details

* fix: replace magic index, template literals, and inline noops in Memories/Traces sidebar

* [autofix.ci] apply automated fixes

* ruff fix

* fix testcases

* improve testcase cov import clean up

* [autofix.ci] apply automated fixes

* fix import order

* accessibility fix

* [autofix.ci] apply automated fixes

* feat(memories): make preprocessing instructions required with tooltip hint

* ruff fix

* [autofix.ci] apply automated fixes

* feat(memories): redesign MemoryDetailsHeader UI and update tests

* [autofix.ci] apply automated fixes

* ruff fixes

---------

Co-authored-by: Olayinka Adelakun <olayinkaadelakun@mac.war.can.ibm.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local>
This commit is contained in:
olayinkaadelakun
2026-05-25 13:42:43 -07:00
committed by GitHub
parent 2ddac97c77
commit f46c8a140c
4 changed files with 131 additions and 131 deletions

View File

@ -1555,7 +1555,6 @@
"memory.noChunksRunFlow": "Run a flow, and chunks will start appearing automatically.",
"memory.noId": "(no id)",
"memory.preprocessing": "Preprocessing:",
"memory.preprocessingPromptPlaceholder": "Produce a concise summary that captures key facts and context.",
"memory.processedMessages": "Processed Messages",
"memory.providerLabel": "Provider:",
"memory.reloadSessions": "Reload sessions and messages",
@ -1566,6 +1565,7 @@
"memory.preprocessingInstructions": "Preprocessing instructions:",
"memory.preprocessingInstructionsHint": "Summarize the key context, facts, and user preferences from this chat to remember for future conversations, skipping the casual chitchat.",
"memory.senderLabel": "Sender:",
"memory.activate": "Activate",
"memory.sessionFilter": "Session filter",
"memory.sessionLabel": "Session:",
"memory.timestampLabel": "Timestamp:",

View File

@ -1,9 +1,9 @@
import type { UIEvent } from "react";
import { useTranslation } from "react-i18next";
import IconComponent from "@/components/common/genericIconComponent";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { cn } from "@/utils/utils";
import { useTranslation } from "react-i18next";
import { SIDEBAR_SCROLL_THRESHOLD_PX } from "../MemoriesMainContent.constants";
import { MemoriesSidebarProps } from "../types";
@ -32,24 +32,23 @@ export function MemoriesSidebar({
return (
<aside className="flex w-72 shrink-0 flex-col border-r border-border bg-background">
<div className="border-b border-border px-4 py-3">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<IconComponent
name="BrainCog"
className="h-4 w-4 text-muted-foreground"
/>
<h2 className="text-sm font-semibold">Memories</h2>
</div>
<Button
size="sm"
variant="outline"
onClick={onCreateMemory}
disabled={!currentFlowId}
>
<IconComponent name="Plus" className="h-3.5 w-3.5" />
Create
</Button>
<div className="flex items-center gap-2">
<IconComponent
name="BrainCog"
className="h-4 w-4 text-muted-foreground"
/>
<h2 className="text-sm font-semibold">Memories</h2>
</div>
<Button
size="sm"
variant="outline"
onClick={onCreateMemory}
disabled={!currentFlowId}
className="mt-3 rounded-[10px]"
>
<IconComponent name="Plus" className="h-3.5 w-3.5" />
Create
</Button>
</div>
<div className="p-4">

View File

@ -1,4 +1,5 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import IconComponent from "@/components/common/genericIconComponent";
import { Button } from "@/components/ui/button";
import {
@ -7,8 +8,8 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Switch } from "@/components/ui/switch";
import DeleteConfirmationModal from "@/modals/deleteConfirmationModal";
import { useTranslation } from "react-i18next";
import useAlertStore from "@/stores/alertStore";
import { extractApiErrorMessages } from "@/utils/apiError";
import { cn } from "@/utils/utils";
@ -47,10 +48,6 @@ export function MemoryDetailsHeader({
}
};
const effectiveSession = (selectedSession ?? sessions?.[0] ?? "") as
| string
| null;
const handleSessionsScroll = (e: React.UIEvent<HTMLDivElement>) => {
const el = e.currentTarget;
if (
@ -62,106 +59,91 @@ export function MemoryDetailsHeader({
}
};
const sessionLabel = selectedSession ?? "All Sessions";
return (
<div className="flex items-center justify-between border-b border-border bg-background px-6 py-3">
<div className="flex items-center gap-3">
<IconComponent
name="BrainCog"
className="h-5 w-5 text-muted-foreground"
/>
<div>
<h2 className="text-sm font-semibold">{memory.name}</h2>
{memory.description && (
<p className="text-xs text-muted-foreground">
{memory.description}
</p>
)}
<div className="flex items-end justify-between border-b border-border bg-background px-6 py-4">
<div className="flex flex-col gap-3">
<h2 className="text-base font-semibold">{memory.name}</h2>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
{t("memory.activate")}
</span>
<Switch
checked={memory.is_active}
onCheckedChange={(checked) => handleToggleActive(checked)}
aria-label={t("memory.toggleAutoCapture")}
className="data-[state=checked]:bg-accent-emerald-foreground"
/>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-4">
{sessions && sessions.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
aria-label={t("memory.sessionFilter")}
disabled={sessions.length <= 1 && !hasNextSessionsPage}
className="w-[240px] justify-between px-3"
>
<span className="truncate">
{effectiveSession && effectiveSession.length > 20
? `${effectiveSession.slice(0, 20)}...`
: (effectiveSession ?? "")}
</span>
<IconComponent
name="ChevronDown"
className="ml-2 h-4 w-4 shrink-0 text-muted-foreground"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[240px] p-0">
<div
className="max-h-[240px] overflow-y-auto py-1"
onScroll={handleSessionsScroll}
>
{sessions.map((sid) => {
const isSelected = sid === effectiveSession;
return (
<DropdownMenuItem
key={sid}
className="flex items-center justify-between"
onSelect={() => {
setSelectedSession(sid);
}}
>
<span className="truncate">{sid}</span>
<IconComponent
name="Check"
className={
isSelected
? "h-4 w-4 text-primary"
: "h-4 w-4 opacity-0"
}
/>
</DropdownMenuItem>
);
})}
{isFetchingNextSessionsPage && (
<div className="py-1 text-center">
<span className="text-xs text-muted-foreground">
Loading
</span>
</div>
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
{t("memory.sessionLabel")}
</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
aria-label={t("memory.sessionFilter")}
className="w-[180px] justify-between rounded-[10px] px-3"
>
<span className="truncate">
{sessionLabel.length > 20
? `${sessionLabel.slice(0, 20)}...`
: sessionLabel}
</span>
<IconComponent
name="ChevronDown"
className="ml-2 h-4 w-4 shrink-0 text-muted-foreground"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[180px] p-0">
<div
className="max-h-[240px] overflow-y-auto py-1"
onScroll={handleSessionsScroll}
>
{sessions.map((sid) => {
const isSelected = sid === selectedSession;
return (
<DropdownMenuItem
key={sid}
className="flex items-center justify-between"
onSelect={() => setSelectedSession(sid)}
>
<span className="truncate">{sid}</span>
<IconComponent
name="Check"
className={
isSelected
? "h-4 w-4 text-primary"
: "h-4 w-4 opacity-0"
}
/>
</DropdownMenuItem>
);
})}
{isFetchingNextSessionsPage && (
<div className="py-1 text-center">
<span className="text-xs text-muted-foreground">
Loading
</span>
</div>
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
<Button
variant={memory.is_active ? "primary" : "outline"}
size="sm"
onClick={() => handleToggleActive((prevIsActive) => !prevIsActive)}
aria-pressed={memory.is_active}
aria-label={t("memory.toggleAutoCapture")}
className="gap-2"
>
<span
className={cn(
"h-2 w-2 shrink-0 rounded-full",
memory.is_active
? "bg-accent-emerald-foreground"
: "bg-muted-foreground",
)}
/>
Auto-capture
</Button>
<Button
variant="outline"
size="sm"
variant="ghost"
size="icon"
onClick={handleRefresh}
disabled={isRefreshing}
aria-label={t("memory.reloadSessions")}
@ -181,12 +163,12 @@ export function MemoryDetailsHeader({
asChild
>
<Button
variant="outline"
size="sm"
variant="ghost"
size="icon"
disabled={deleteMutation.isPending}
aria-label="Delete memory"
>
<IconComponent name="Trash2" className="h-4 w-4" />
Delete
<IconComponent name="Trash2" className="h-4 w-4 text-destructive" />
</Button>
</DeleteConfirmationModal>
</div>

View File

@ -26,6 +26,25 @@ jest.mock("@/components/common/genericIconComponent", () => ({
default: ({ name }: { name: string }) => <span>{name}</span>,
}));
jest.mock("@/components/ui/switch", () => ({
Switch: ({
checked,
onCheckedChange,
"aria-label": ariaLabel,
}: {
checked: boolean;
onCheckedChange: (v: boolean) => void;
"aria-label"?: string;
}) => (
<button
role="switch"
aria-checked={checked}
aria-label={ariaLabel}
onClick={() => onCheckedChange(!checked)}
/>
),
}));
jest.mock("@/components/ui/dropdown-menu", () => ({
DropdownMenu: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
@ -121,9 +140,10 @@ describe("MemoryDetailsHeader", () => {
});
render(<MemoryDetailsHeader {...props} />);
expect(screen.getByText("Memory One")).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Toggle auto-capture" }),
).toHaveTextContent("Auto-capture");
expect(screen.getByText("Activate")).toBeInTheDocument();
const toggle = screen.getByRole("switch", { name: "Toggle auto-capture" });
expect(toggle).toBeInTheDocument();
expect(toggle).toHaveAttribute("aria-checked", "true");
});
it("calls mutate handlers for actions", () => {
@ -137,16 +157,15 @@ describe("MemoryDetailsHeader", () => {
});
it("toggles auto-capture", () => {
const props = makeProps();
const props = makeProps({
memory: { ...makeProps().memory, is_active: true },
});
render(<MemoryDetailsHeader {...props} />);
fireEvent.click(
screen.getByRole("button", { name: "Toggle auto-capture" }),
screen.getByRole("switch", { name: "Toggle auto-capture" }),
);
const firstCallArg = (props.handleToggleActive as jest.Mock).mock
.calls[0]?.[0];
expect(firstCallArg).toEqual(expect.any(Function));
expect(firstCallArg(true)).toBe(false);
expect(props.handleToggleActive).toHaveBeenCalledWith(false);
});
it("renders the session selector when sessions exist", () => {
@ -160,7 +179,7 @@ describe("MemoryDetailsHeader", () => {
expect(sessionTrigger).not.toBeDisabled();
});
it("disables the session selector when only one session and no more pages", () => {
it("always enables the session selector regardless of session count", () => {
const props = makeProps({
sessions: ["session-1"],
hasNextSessionsPage: false,
@ -168,7 +187,7 @@ describe("MemoryDetailsHeader", () => {
render(<MemoryDetailsHeader {...props} />);
expect(
screen.getByRole("button", { name: "Session filter" }),
).toBeDisabled();
).not.toBeDisabled();
});
it("renders the reload button with correct aria-label", () => {