Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd0a6342b7 |
No files matched your search
@@ -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") => (
|
||||
<LanguageProvider>
|
||||
<MemoryModeCards
|
||||
aria-label="记忆模式"
|
||||
value={value}
|
||||
layout="rows"
|
||||
onChange={onChange}
|
||||
ultraDetail={
|
||||
<div>
|
||||
<strong>每天自动整合</strong>
|
||||
<small>默认 03:00,可在设置中修改。</small>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</LanguageProvider>
|
||||
);
|
||||
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(
|
||||
<LanguageProvider>
|
||||
<MemoryModeCards
|
||||
aria-label="记忆模式"
|
||||
value="lite"
|
||||
disabled={(mode) => mode !== "lite"}
|
||||
onChange={vi.fn()}
|
||||
/>
|
||||
</LanguageProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("radio", { name: /基础记忆/ })).toHaveProperty(
|
||||
"disabled",
|
||||
false,
|
||||
);
|
||||
expect(screen.getByRole("radio", { name: /智能记忆/ })).toHaveProperty(
|
||||
"disabled",
|
||||
true,
|
||||
);
|
||||
expect(screen.getByRole("radio", { name: /深度记忆/ })).toHaveProperty(
|
||||
"disabled",
|
||||
true,
|
||||
);
|
||||
});
|
||||
@@ -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 (
|
||||
<div
|
||||
className="memory-mode-cards"
|
||||
className={`memory-mode-cards memory-mode-cards--${layout}`}
|
||||
role="radiogroup"
|
||||
aria-label={ariaLabel}
|
||||
aria-busy={ariaBusy || undefined}
|
||||
>
|
||||
{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 (
|
||||
<div
|
||||
key={mode.id}
|
||||
className="memory-mode-card"
|
||||
data-mode={mode.id}
|
||||
data-selected={selected || undefined}
|
||||
data-current={current === mode.id || undefined}
|
||||
data-disabled={modeDisabled || undefined}
|
||||
>
|
||||
<label className="memory-mode-card-choice">
|
||||
<input
|
||||
type="radio"
|
||||
name={name}
|
||||
value={mode.id}
|
||||
aria-label={localizedTitle}
|
||||
checked={selected}
|
||||
disabled={modeDisabled}
|
||||
onChange={(event) => onChange(mode.id, event.currentTarget)}
|
||||
/>
|
||||
<span className="memory-mode-title">
|
||||
<span className="memory-mode-index">{mode.index}</span>
|
||||
<strong>{title}</strong>
|
||||
<span className="memory-mode-tier">
|
||||
{mode.id.toUpperCase()}
|
||||
</span>
|
||||
{current === mode.id && currentBadge ? (
|
||||
<span className="memory-mode-current">{currentBadge}</span>
|
||||
) : null}
|
||||
<span className="memory-mode-radio" aria-hidden="true">
|
||||
{selected ? <Check size={13} /> : null}
|
||||
</span>
|
||||
</span>
|
||||
<span className="memory-mode-description">
|
||||
{text(mode.description)}
|
||||
</span>
|
||||
<span className="memory-mode-features">
|
||||
{features.map((feature) => (
|
||||
<span key={feature}>
|
||||
<i aria-hidden="true" />
|
||||
{text(feature)}
|
||||
<div className="memory-mode-card-list">
|
||||
{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 (
|
||||
<div
|
||||
key={mode.id}
|
||||
className="memory-mode-card"
|
||||
data-mode={mode.id}
|
||||
data-selected={selected || undefined}
|
||||
data-current={current === mode.id || undefined}
|
||||
data-disabled={modeDisabled || undefined}
|
||||
>
|
||||
<label className="memory-mode-card-choice">
|
||||
<input
|
||||
type="radio"
|
||||
name={name}
|
||||
value={mode.id}
|
||||
aria-label={localizedTitle}
|
||||
checked={selected}
|
||||
disabled={modeDisabled}
|
||||
onChange={(event) => onChange(mode.id, event.currentTarget)}
|
||||
/>
|
||||
<span className="memory-mode-title">
|
||||
<span className="memory-mode-index">{mode.index}</span>
|
||||
<strong>{title}</strong>
|
||||
<span className="memory-mode-tier">
|
||||
{mode.id.toUpperCase()}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</label>
|
||||
{mode.id === "ultra" && selected && ultraDetail ? (
|
||||
<div className="memory-mode-detail">{ultraDetail}</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{current === mode.id && currentBadge ? (
|
||||
<span className="memory-mode-current">{currentBadge}</span>
|
||||
) : null}
|
||||
<span className="memory-mode-radio" aria-hidden="true">
|
||||
{selected ? <Check size={13} /> : null}
|
||||
</span>
|
||||
</span>
|
||||
<span className="memory-mode-description">
|
||||
{text(mode.description)}
|
||||
</span>
|
||||
<span className="memory-mode-features">
|
||||
{features.map((feature) => (
|
||||
<span key={feature}>
|
||||
<i aria-hidden="true" />
|
||||
{text(feature)}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
{mode.id === "ultra" && selected && ultraDetail ? (
|
||||
<div className="memory-mode-detail">
|
||||
<Clock3 size={14} aria-hidden="true" />
|
||||
<div>{ultraDetail}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1266,6 +1266,7 @@ export function OnboardingFlow({
|
||||
<MemoryModeCards
|
||||
aria-label={text("记忆模式")}
|
||||
value={modeChoice}
|
||||
layout="rows"
|
||||
disabled={modeSaving || completionBusy}
|
||||
onChange={(mode) => {
|
||||
modeChoiceTouched.current = true;
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
.memory-mode-cards {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
min-height: 420px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.memory-mode-card-list {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.memory-mode-card {
|
||||
position: relative;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 286px;
|
||||
min-height: 128px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -43,10 +55,15 @@
|
||||
}
|
||||
|
||||
.memory-mode-card-choice {
|
||||
min-height: 284px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
padding: 18px 20px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(230px, 0.85fr) minmax(0, 1.45fr);
|
||||
grid-template-rows: auto auto minmax(0, 1fr);
|
||||
column-gap: 28px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -68,10 +85,13 @@
|
||||
}
|
||||
|
||||
.memory-mode-title {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-right: 34px;
|
||||
}
|
||||
|
||||
.memory-mode-index {
|
||||
@@ -110,9 +130,11 @@
|
||||
}
|
||||
|
||||
.memory-mode-radio {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 20px;
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
margin-left: auto;
|
||||
flex: 0 0 21px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -133,8 +155,11 @@
|
||||
}
|
||||
|
||||
.memory-mode-description {
|
||||
min-height: 50px;
|
||||
margin-top: 10px;
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
align-self: start;
|
||||
min-height: 0;
|
||||
margin-top: 6px;
|
||||
color: #526674;
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
@@ -142,11 +167,16 @@
|
||||
}
|
||||
|
||||
.memory-mode-features {
|
||||
margin-top: auto;
|
||||
padding: 16px 0 2px;
|
||||
grid-column: 2;
|
||||
grid-row: 1 / -1;
|
||||
align-self: stretch;
|
||||
margin-top: 0;
|
||||
padding: 0 42px 0 28px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
align-content: center;
|
||||
gap: 8px;
|
||||
border-top: 1px solid #dbe4ea;
|
||||
border-left: 1px solid #dbe4ea;
|
||||
color: #5e6e7a;
|
||||
font-size: 12px;
|
||||
line-height: 19px;
|
||||
@@ -168,14 +198,97 @@
|
||||
}
|
||||
|
||||
.memory-mode-detail {
|
||||
margin-top: auto;
|
||||
padding: 14px 18px;
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
align-self: start;
|
||||
margin-top: 6px;
|
||||
padding: 7px 0 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 7px;
|
||||
border-top: 1px solid #d4e0e7;
|
||||
background: #f1f6f8;
|
||||
background: transparent;
|
||||
color: #405867;
|
||||
font-size: 11px;
|
||||
line-height: 17px;
|
||||
animation: memory-mode-detail-in 180ms ease-out both;
|
||||
}
|
||||
|
||||
.memory-mode-detail > svg {
|
||||
margin-top: 1px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.memory-mode-detail > div {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid {
|
||||
flex: none;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-card-list {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-card {
|
||||
position: static;
|
||||
flex: none;
|
||||
min-height: 286px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-card-choice {
|
||||
position: static;
|
||||
min-height: 284px;
|
||||
height: auto;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-title {
|
||||
width: 100%;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-radio {
|
||||
position: static;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-description {
|
||||
min-height: 50px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-features {
|
||||
width: 100%;
|
||||
margin-top: auto;
|
||||
padding: 16px 0 2px;
|
||||
border-top: 1px solid #dbe4ea;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-detail {
|
||||
width: calc(100% + 40px);
|
||||
margin: auto -20px -20px;
|
||||
padding: 14px 18px;
|
||||
display: block;
|
||||
border-radius: 0 0 15px 15px;
|
||||
background: #f1f6f8;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
animation: memory-mode-detail-in 180ms ease-out both;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-detail > svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes memory-mode-detail-in {
|
||||
@@ -185,34 +298,131 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 681px) and (max-height: 959px) {
|
||||
.memory-mode-cards {
|
||||
gap: 14px;
|
||||
@media (min-width: 761px) and (max-height: 959px) {
|
||||
.memory-mode-cards--rows {
|
||||
min-height: 360px;
|
||||
}
|
||||
.memory-mode-card {
|
||||
min-height: 194px;
|
||||
.memory-mode-cards--rows .memory-mode-card-list {
|
||||
gap: 12px;
|
||||
}
|
||||
.memory-mode-card-choice {
|
||||
min-height: 194px;
|
||||
padding: 16px;
|
||||
.memory-mode-cards--rows .memory-mode-card {
|
||||
min-height: 112px;
|
||||
}
|
||||
.memory-mode-title strong {
|
||||
.memory-mode-cards--rows .memory-mode-card-choice {
|
||||
min-height: 0;
|
||||
padding: 13px 16px;
|
||||
grid-template-columns: minmax(205px, 0.82fr) minmax(0, 1.5fr);
|
||||
column-gap: 22px;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-radio {
|
||||
top: 13px;
|
||||
right: 16px;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-title strong {
|
||||
font-size: 17px;
|
||||
line-height: 25px;
|
||||
}
|
||||
.memory-mode-description {
|
||||
.memory-mode-cards--rows .memory-mode-description {
|
||||
min-height: 0;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-features {
|
||||
margin-top: 0;
|
||||
padding: 0 36px 0 22px;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
line-height: 17px;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-detail {
|
||||
margin-top: 5px;
|
||||
padding: 5px 0 0;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.memory-mode-cards--rows {
|
||||
flex: none;
|
||||
min-height: 0;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-card-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-card {
|
||||
flex: none;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-card-choice {
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-radio {
|
||||
position: static;
|
||||
margin-left: auto;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-title {
|
||||
width: 100%;
|
||||
padding-right: 0;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-description {
|
||||
min-height: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-features {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
padding: 16px 0 0;
|
||||
border-top: 1px solid #dbe4ea;
|
||||
border-left: 0;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-detail {
|
||||
width: calc(100% + 40px);
|
||||
margin: 16px -20px -20px;
|
||||
padding: 14px 20px 16px;
|
||||
border-radius: 0 0 15px 15px;
|
||||
background: #f1f6f8;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 681px) and (max-height: 959px) {
|
||||
.memory-mode-cards--grid {
|
||||
gap: 14px;
|
||||
}
|
||||
.memory-mode-cards--grid .memory-mode-card {
|
||||
min-height: 194px;
|
||||
}
|
||||
.memory-mode-cards--grid .memory-mode-card-choice {
|
||||
min-height: 194px;
|
||||
padding: 16px;
|
||||
}
|
||||
.memory-mode-cards--grid .memory-mode-title strong {
|
||||
font-size: 17px;
|
||||
line-height: 25px;
|
||||
}
|
||||
.memory-mode-cards--grid .memory-mode-description {
|
||||
min-height: 36px;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.memory-mode-features {
|
||||
.memory-mode-cards--grid .memory-mode-features {
|
||||
padding-top: 10px;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.memory-mode-detail {
|
||||
.memory-mode-cards--grid .memory-mode-detail {
|
||||
width: calc(100% + 32px);
|
||||
margin: auto -16px -16px;
|
||||
padding: 10px 14px;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
@@ -220,14 +430,14 @@
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.memory-mode-cards {
|
||||
.memory-mode-cards--grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.memory-mode-card,
|
||||
.memory-mode-card-choice {
|
||||
.memory-mode-cards--grid .memory-mode-card,
|
||||
.memory-mode-cards--grid .memory-mode-card-choice {
|
||||
min-height: 0;
|
||||
}
|
||||
.memory-mode-description {
|
||||
.memory-mode-cards--grid .memory-mode-description {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,6 +634,12 @@
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
@media (min-width: 761px) {
|
||||
.onboarding-r6-mode {
|
||||
min-height: 100%;
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
.onboarding-r6-empty {
|
||||
margin: 0;
|
||||
padding: 28px 18px;
|
||||
|
||||
Reference in new issue
Block a user