diff --git a/memhub/apps/web/src/MemoryModeCards.test.tsx b/memhub/apps/web/src/MemoryModeCards.test.tsx new file mode 100644 index 0000000..a369d52 --- /dev/null +++ b/memhub/apps/web/src/MemoryModeCards.test.tsx @@ -0,0 +1,70 @@ +// @vitest-environment jsdom + +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, expect, it, vi } from "vitest"; +import { LanguageProvider } from "./gui-language"; +import { MemoryModeCards } from "./MemoryModeCards"; + +afterEach(cleanup); + +it("shows every mode feature and reveals Ultra scheduling after selection", async () => { + const onChange = vi.fn(); + const user = userEvent.setup(); + const view = (value: "lite" | "pro" | "ultra") => ( + + + 每天自动整合 + 默认 03:00,可在设置中修改。 + + } + /> + + ); + const { rerender } = render(view("pro")); + + expect(screen.getByRole("radiogroup", { name: "记忆模式" })).toBeTruthy(); + expect(screen.getAllByRole("radio")).toHaveLength(3); + expect(screen.getByText("不调用生成模型")).toBeTruthy(); + expect(screen.getByText("沉淀高级记忆")).toBeTruthy(); + expect(screen.getByText("增强关联召回")).toBeTruthy(); + expect(screen.queryByText("每天自动整合")).toBeNull(); + + await user.click(screen.getByRole("radio", { name: /深度记忆/ })); + expect(onChange).toHaveBeenCalledWith("ultra", expect.any(HTMLInputElement)); + + rerender(view("ultra")); + expect(screen.getByText("每天自动整合")).toBeTruthy(); +}); + +it("preserves per-mode disabled behavior", () => { + render( + + mode !== "lite"} + onChange={vi.fn()} + /> + , + ); + + expect(screen.getByRole("radio", { name: /基础记忆/ })).toHaveProperty( + "disabled", + false, + ); + expect(screen.getByRole("radio", { name: /智能记忆/ })).toHaveProperty( + "disabled", + true, + ); + expect(screen.getByRole("radio", { name: /深度记忆/ })).toHaveProperty( + "disabled", + true, + ); +}); diff --git a/memhub/apps/web/src/MemoryModeCards.tsx b/memhub/apps/web/src/MemoryModeCards.tsx index 5b5d55a..6e4d0d2 100644 --- a/memhub/apps/web/src/MemoryModeCards.tsx +++ b/memhub/apps/web/src/MemoryModeCards.tsx @@ -1,5 +1,5 @@ import type { MemoryPipelineMode } from "@one-memory/contracts"; -import { Check } from "lucide-react"; +import { Check, Clock3 } from "lucide-react"; import { type ReactNode, useId } from "react"; import { MODE_COPY } from "./memory-mode-ui"; import { useOnboardingTranslation } from "./onboarding-language"; @@ -32,6 +32,7 @@ export function MemoryModeCards({ current, currentBadge, disabled = false, + layout = "grid", onChange, ultraDetail, "aria-label": ariaLabel, @@ -41,6 +42,7 @@ export function MemoryModeCards({ current?: MemoryPipelineMode; currentBadge?: ReactNode; disabled?: boolean | ((mode: MemoryPipelineMode) => boolean); + layout?: "grid" | "rows"; onChange(mode: MemoryPipelineMode, control: HTMLInputElement): void; ultraDetail?: ReactNode; "aria-label": string; @@ -53,70 +55,77 @@ export function MemoryModeCards({ return (
- {MODES.map((mode) => { - const selected = value === mode.id; - const modeDisabled = unavailable(mode.id); - const localizedTitle = text(MODE_COPY[mode.id].title); - const title = localizedTitle.replace( - /\s*[((](?:lite|pro|ultra)[))]\s*$/i, - "", - ); - const features = text(MODE_COPY[mode.id].description).split(/\s*·\s*/); - return ( -
-