Compare commits

...
Author SHA1 Message Date
kristinheandCursor 9030aac03d fix(memhub): restore v1.0.4 connection step
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-06 13:52:08 +08:00
kristinheandCursor 1b43c53806 fix(memhub): polish cold-start setup layout
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-06 13:52:08 +08:00
10 changed files with 357 additions and 375 deletions

No files matched your search

+118 -116
View File
@@ -266,93 +266,9 @@ export function FirstImportReview({
step={2}
title={text(title)}
footer={
<div className="first-import-r6-footer-actions">
{changing ? (
<R6Button
onClick={() => {
stopBatch.current = true;
}}
>
{text("停止后续操作")}
</R6Button>
) : onChangeJob && (canPause || canResume) ? (
<R6Button
disabled={busy || (stale && !canPause)}
onClick={() => void changeBatch(canPause ? "pause" : "resume")}
>
{canPause ? (
<Pause size={16} aria-hidden="true" />
) : (
<Play size={16} aria-hidden="true" />
)}
{text(canPause ? "暂停导入" : "继续导入")}
</R6Button>
) : null}
{recovery ? (
<>
<R6Button
disabled={busy}
onClick={() => {
stopBatch.current = true;
recovery.onSkip();
}}
>
{text(
recovery.destination === "sources"
? "仍进入应用页"
: recovery.destination === "settings"
? "仍进入设置"
: "仍进入工作台",
)}
</R6Button>
<R6Button
variant="primary"
disabled={busy}
onClick={() => leave(recovery.destination)}
>
{text("重试保存并进入")}
</R6Button>
</>
) : (
<R6Button
variant="primary"
disabled={busy}
onClick={() => leave("settings")}
>
{text("选择记忆模式")}
</R6Button>
)}
</div>
}
>
<div className="first-import-r6-layout">
<section
className="first-import-r6-main"
aria-label={text("导入与原文")}
>
<div className="first-import-r6-intro">
<h2>{text("正在导入你的会话")}</h2>
{importSubmissionState === "checking" ? (
<p className="first-import-r6-submission-status" role="status">
{t("onboardingImportChecking")}
</p>
) : (
<p>
{text(
allCompleted
? "本次导入已结束。已保存的对话无需重复导入。"
: "后台将持续为你导入,可以放心进入下一步",
)}
</p>
)}
{ids.length > 3 && (
<p className="first-import-r6-scroll-hint">
{t("onboardingMoreImportApps", { count: ids.length - 3 })}
</p>
)}
</div>
{(error || actionMessage) && (
<div className="first-import-r6-feedback">
<>
<div className="first-import-r6-footer-note">
{(error || actionMessage) && (
<p
role={
error?.includes("应用连接") ||
@@ -371,8 +287,87 @@ export function FirstImportReview({
>
{error ? text(error) : actionMessage}
</p>
</div>
)}
)}
</div>
<div className="first-import-r6-footer-actions">
{changing ? (
<R6Button
onClick={() => {
stopBatch.current = true;
}}
>
{text("停止后续操作")}
</R6Button>
) : onChangeJob && (canPause || canResume) ? (
<R6Button
disabled={busy || (stale && !canPause)}
onClick={() => void changeBatch(canPause ? "pause" : "resume")}
>
{canPause ? (
<Pause size={16} aria-hidden="true" />
) : (
<Play size={16} aria-hidden="true" />
)}
{text(canPause ? "暂停导入" : "继续导入")}
</R6Button>
) : null}
{recovery ? (
<>
<R6Button
disabled={busy}
onClick={() => {
stopBatch.current = true;
recovery.onSkip();
}}
>
{text(
recovery.destination === "sources"
? "仍进入应用页"
: recovery.destination === "settings"
? "仍进入设置"
: "仍进入工作台",
)}
</R6Button>
<R6Button
variant="primary"
disabled={busy}
onClick={() => leave(recovery.destination)}
>
{text("重试保存并进入")}
</R6Button>
</>
) : (
<R6Button
variant="primary"
disabled={busy}
onClick={() => leave("settings")}
>
{text("选择记忆模式")}
</R6Button>
)}
</div>
</>
}
>
<div className="first-import-r6-layout">
<section
className="first-import-r6-main"
aria-label={text("导入与原文")}
>
<p className="first-import-r6-intro">
{importSubmissionState === "checking"
? t("onboardingImportChecking")
: text(
allCompleted
? "本次导入已结束。已保存的对话无需重复导入。"
: "对话先保存在本机。你可以同时设置后续连接。",
)}
</p>
<p className="first-import-r6-subintro">
{text(
"导入不会自动开放应用读取权限。配置模型后,才会开始生成记忆。",
)}
</p>
{historyWindowLimited && (
<p className="first-import-r6-history-window" role="status">
{t("onboardingHistoryWindow")}
@@ -384,39 +379,46 @@ export function FirstImportReview({
jobs={batch}
stale={progressStale}
/>
{Object.keys(failedSources).length > 0 && (
<section
className="first-import-r6-failures"
aria-label={text("未开始的导入")}
>
{Object.keys(failedSources).map((id) => (
<p key={id}>
{sources.find((source) => source.source_id === id)
?.display_name ?? id}{" "}
{text("未能开始导入,其他已接受任务不受影响。")}
</p>
))}
<R6Button
variant="quiet"
disabled={busy}
onClick={() => leave("sources")}
<section
className="first-import-r6-feed"
aria-label={text("导入说明")}
>
<h2>{text("正在导入你的会话")}</h2>
<p className="first-import-r6-feed-status">
{text("后台将持续为你导入,可以放心进入下一步")}
</p>
{Object.keys(failedSources).length > 0 && (
<section
className="first-import-r6-failures"
aria-label={text("未开始的导入")}
>
{text("查看未导入的应用")}
</R6Button>
</section>
)}
{Object.keys(failedSources).map((id) => (
<p key={id}>
{sources.find((source) => source.source_id === id)
?.display_name ?? id}{" "}
{text("未能开始导入,其他已接受任务不受影响。")}
</p>
))}
<R6Button
variant="quiet"
disabled={busy}
onClick={() => leave("sources")}
>
{text("查看未导入的应用")}
</R6Button>
</section>
)}
</section>
</section>
<aside
className="first-import-r6-options"
aria-label={text("连接应用")}
>
<div className="first-import-r6-options-head">
<p className="first-import-r6-eyebrow">{text("让记忆继续生长")}</p>
<h2>{text("连接你的应用")}</h2>
<p className="first-import-r6-options-intro">
{text("适用于本次选择且支持的应用。")}
</p>
</div>
<p className="first-import-r6-eyebrow">{text("让记忆继续生长")}</p>
<h2>{text("连接你的应用")}</h2>
<p className="first-import-r6-options-intro">
{text("适用于本次选择且支持的应用。")}
</p>
<SelectionTile
title={text(
hostConnectionAvailable ? "一键接入应用" : "测试实例不接入应用",
+3
View File
@@ -33,6 +33,7 @@ export function MemoryModeCards({
currentBadge,
disabled = false,
layout = "grid",
describedBy,
onChange,
ultraDetail,
"aria-label": ariaLabel,
@@ -43,6 +44,7 @@ export function MemoryModeCards({
currentBadge?: ReactNode;
disabled?: boolean | ((mode: MemoryPipelineMode) => boolean);
layout?: "grid" | "rows";
describedBy?: Partial<Record<MemoryPipelineMode, string | undefined>>;
onChange(mode: MemoryPipelineMode, control: HTMLInputElement): void;
ultraDetail?: ReactNode;
"aria-label": string;
@@ -87,6 +89,7 @@ export function MemoryModeCards({
name={name}
value={mode.id}
aria-label={localizedTitle}
aria-describedby={describedBy?.[mode.id]}
checked={selected}
disabled={modeDisabled}
onChange={(event) => onChange(mode.id, event.currentTarget)}
@@ -156,7 +156,7 @@ it("moves from import to the actual third step without completing onboarding", a
);
await userEvent.click(screen.getByRole("button", { name: "进入第三步" }));
await userEvent.click(screen.getByRole("radio", { name: /pro/i }));
await userEvent.click(screen.getByRole("button", { name: "继续配置模型" }));
await userEvent.click(screen.getByRole("button", { name: "配置模型" }));
expect(screen.getByRole("heading", { name: "第三步模型页面" })).toBeTruthy();
expect(completeOnboarding).not.toHaveBeenCalled();
expect(finish).not.toHaveBeenCalled();
+52 -24
View File
@@ -240,7 +240,7 @@ async function continueImport(user: ReturnType<typeof userEvent.setup>) {
async function openModelStep(user: ReturnType<typeof userEvent.setup>) {
await user.click(screen.getByRole("button", { name: "选择记忆模式" }));
await selectMode(user, /pro/i);
await user.click(screen.getByRole("button", { name: "继续配置模型" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
}
async function chooseLiteAndContinue(user: ReturnType<typeof userEvent.setup>) {
await selectMode(user, /lite/i);
@@ -688,7 +688,7 @@ describe("OnboardingFlow S1.3", () => {
await openApps();
await user.click(screen.getByRole("button", { name: SKIP }));
await selectMode(user, /pro/i);
await user.click(screen.getByRole("button", { name: "继续配置模型" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
const receipt = screen.getByRole("button", {
name: "模型启用回执已确认",
});
@@ -1031,6 +1031,12 @@ describe("OnboardingFlow S1.3", () => {
await user.click(screen.getByRole("button", { name: SKIP }));
expect(screen.getByRole("heading", { name: "选择记忆模式" })).toBeTruthy();
expect(
screen.getByRole("heading", {
name: "你希望 MemHub 如何形成记忆?",
}),
).toBeTruthy();
expect(screen.queryByText("Choose how MemHub remembers")).toBeNull();
expect(screen.getByText("之后可在设置中更改。")).toBeTruthy();
expect(screen.getAllByRole("radio")).toHaveLength(3);
expect(screen.getByRole("radio", { name: /ultra/i })).toBeTruthy();
@@ -1043,7 +1049,7 @@ describe("OnboardingFlow S1.3", () => {
await selectMode(user, /lite/i);
expect(screen.getByRole("button", { name: "使用 Lite 进入" })).toBeTruthy();
await selectMode(user, /pro/i);
expect(screen.getByRole("button", { name: "继续配置模型" })).toBeTruthy();
expect(screen.getByRole("button", { name: "配置模型" })).toBeTruthy();
});
it("configures Ultra scheduling before model setup and completes after activation", async () => {
@@ -1083,10 +1089,18 @@ describe("OnboardingFlow S1.3", () => {
await openApps();
await user.click(screen.getByRole("button", { name: SKIP }));
await user.click(screen.getByRole("radio", { name: /ultra/i }));
const ultra = screen.getByRole("radio", { name: /ultra/i });
await user.click(ultra);
const schedule = screen.getByRole("complementary", {
name: "每天自动整合",
});
expect(schedule.closest(".memory-mode-card")).toBeNull();
expect(ultra.getAttribute("aria-describedby")).toBe(
"onboarding-ultra-schedule-description",
);
expect(screen.getByText("每天自动整合")).toBeTruthy();
expect(screen.getByText(/每天 03:00 左右自动整合/)).toBeTruthy();
await user.click(screen.getByRole("button", { name: "继续" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
expect(screen.getByRole("region", { name: "模型步骤边界" })).toBeTruthy();
expect(putOnboardingProgress).toHaveBeenCalledWith(
expect.objectContaining({
@@ -1149,14 +1163,14 @@ describe("OnboardingFlow S1.3", () => {
}),
);
await waitFor(() =>
expect(screen.getByRole("button", { name: "继续" })).toHaveProperty(
expect(screen.getByRole("button", { name: "配置模型" })).toHaveProperty(
"disabled",
false,
),
);
expect(screen.getByText(/重新开启后仍按 05:30 运行/)).toBeTruthy();
await user.click(screen.getByRole("button", { name: "继续" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
await waitFor(() =>
expect(finish).toHaveBeenCalledExactlyOnceWith("sources"),
);
@@ -1190,7 +1204,7 @@ describe("OnboardingFlow S1.3", () => {
await openApps();
await user.click(screen.getByRole("button", { name: SKIP }));
await selectMode(user, /ultra/i);
await user.click(screen.getByRole("button", { name: "继续" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
expect((await screen.findByRole("alert")).textContent).toContain(
"自动整合设置未能保存",
);
@@ -1199,7 +1213,7 @@ describe("OnboardingFlow S1.3", () => {
screen.getByRole<HTMLInputElement>("radio", { name: /ultra/i }).checked,
).toBe(true);
await user.click(screen.getByRole("button", { name: "继续" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
await waitFor(() =>
expect(finish).toHaveBeenCalledExactlyOnceWith("sources"),
);
@@ -1207,9 +1221,14 @@ describe("OnboardingFlow S1.3", () => {
});
it("retries a failed Ultra schedule read without losing the selection", async () => {
let resolveRetry!: (value: { enabled: boolean; time: string }) => void;
vi.mocked(api.getEvolveSchedule)
.mockRejectedValueOnce(new Error("offline"))
.mockResolvedValueOnce({ enabled: true, time: "04:15" });
.mockReturnValueOnce(
new Promise((resolve) => {
resolveRetry = resolve;
}),
);
const user = userEvent.setup();
render(
<OnboardingFlow
@@ -1225,19 +1244,28 @@ describe("OnboardingFlow S1.3", () => {
expect(screen.getByRole("alert").textContent).toContain(
"记忆模式状态暂时无法读取",
);
expect(screen.getByRole("button", { name: "继续" })).toHaveProperty(
expect(screen.getByRole("button", { name: "配置模型" })).toHaveProperty(
"disabled",
true,
);
await user.click(screen.getByRole("button", { name: "重试" }));
const retry = screen.getByRole("button", { name: "重试" });
retry.focus();
await user.click(retry);
expect(screen.getByRole("button", { name: "重试" })).toHaveProperty(
"disabled",
true,
);
expect(document.activeElement).toBe(retry);
expect(api.getEvolveSchedule).toHaveBeenCalledTimes(2);
await act(async () => resolveRetry({ enabled: true, time: "04:15" }));
await waitFor(() =>
expect(screen.getByText(/每天 04:15 左右自动整合/)).toBeTruthy(),
);
expect(
screen.getByRole<HTMLInputElement>("radio", { name: /ultra/i }).checked,
).toBe(true);
expect(screen.getByRole("button", { name: "继续" })).toHaveProperty(
expect(screen.getByRole("button", { name: "配置模型" })).toHaveProperty(
"disabled",
false,
);
@@ -1430,7 +1458,7 @@ describe("OnboardingFlow S1.3", () => {
await openApps();
await user.click(screen.getByRole("button", { name: SKIP }));
await selectMode(user, /pro/i);
await user.click(screen.getByRole("button", { name: "继续配置模型" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
await user.click(
screen.getByRole("button", { name: "模型启用回执已确认" }),
);
@@ -1484,7 +1512,7 @@ describe("OnboardingFlow S1.3", () => {
await openApps();
await user.click(screen.getByRole("button", { name: SKIP }));
await selectMode(user, /pro/i);
await user.click(screen.getByRole("button", { name: "继续配置模型" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
await user.click(
screen.getByRole("button", { name: "模型启用回执已确认" }),
);
@@ -1492,7 +1520,7 @@ describe("OnboardingFlow S1.3", () => {
await user.click(screen.getByRole("button", { name: "返回导入" }));
modelActive = false;
await user.click(screen.getByRole("button", { name: "继续配置模型" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
await user.click(
screen.getByRole("button", { name: "模型启用回执已确认" }),
);
@@ -1538,7 +1566,7 @@ describe("OnboardingFlow S1.3", () => {
await openApps();
await user.click(screen.getByRole("button", { name: SKIP }));
await selectMode(user, /pro/i);
await user.click(screen.getByRole("button", { name: "继续配置模型" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
await user.click(
screen.getByRole("button", { name: "模型启用回执已确认" }),
);
@@ -1580,7 +1608,7 @@ describe("OnboardingFlow S1.3", () => {
await openApps();
await user.click(screen.getByRole("button", { name: SKIP }));
await selectMode(user, /pro/i);
await user.click(screen.getByRole("button", { name: "继续配置模型" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
const revisionBeforePut = confirmedMemoryModeSnapshot().revision;
await user.click(
screen.getByRole("button", { name: "模型启用回执已确认" }),
@@ -1629,7 +1657,7 @@ describe("OnboardingFlow S1.3", () => {
await openApps();
await user.click(screen.getByRole("button", { name: SKIP }));
await selectMode(user, /pro/i);
await user.click(screen.getByRole("button", { name: "继续配置模型" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
await user.click(
screen.getByRole("button", { name: "模型启用回执已确认" }),
);
@@ -1669,7 +1697,7 @@ describe("OnboardingFlow S1.3", () => {
await waitFor(() => expect(document.activeElement).toBe(firstTitle));
await selectMode(user, /pro/i);
await user.click(screen.getByRole("button", { name: "继续配置模型" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
await user.click(screen.getByRole("button", { name: "返回导入" }));
const returnedTitle = screen.getByRole("heading", {
name: "选择记忆模式",
@@ -1699,7 +1727,7 @@ describe("OnboardingFlow S1.3", () => {
await openApps();
await user.click(screen.getByRole("button", { name: SKIP }));
await selectMode(user, /pro/i);
await user.click(screen.getByRole("button", { name: "继续配置模型" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
await waitFor(() => expect(finish).toHaveBeenCalledWith("sources"));
expect(screen.queryByRole("alert")).toBeNull();
expect(setMemoryMode).not.toHaveBeenCalled();
@@ -1718,7 +1746,7 @@ describe("OnboardingFlow S1.3", () => {
await openApps();
await user.click(screen.getByRole("button", { name: SKIP }));
await selectMode(user, /pro/i);
await user.click(screen.getByRole("button", { name: "继续配置模型" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
const lateReceipt = screen.getByRole("button", {
name: "模型启用回执已确认",
});
@@ -1767,14 +1795,14 @@ describe("OnboardingFlow S1.3", () => {
await user.click(screen.getByRole("button", { name: SKIP }));
const pro = screen.getByRole<HTMLInputElement>("radio", { name: /pro/i });
await selectMode(user, /pro/i);
await user.click(screen.getByRole("button", { name: "继续配置模型" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
expect((await screen.findByRole("alert")).textContent).toContain(
"模式未能保存",
);
expect(pro.checked).toBe(true);
expect(screen.queryByRole("region", { name: "模型步骤边界" })).toBeNull();
await user.click(screen.getByRole("button", { name: "继续配置模型" }));
await user.click(screen.getByRole("button", { name: "配置模型" }));
expect(
await screen.findByRole("region", { name: "模型步骤边界" }),
).toBeTruthy();
+47 -22
View File
@@ -7,7 +7,7 @@ import type {
SourceResponse,
} from "@one-memory/contracts";
import { legacyMemoryModePlan } from "@one-memory/operations";
import { Check, X } from "lucide-react";
import { Check, Clock3, X } from "lucide-react";
import {
type CSSProperties,
type ReactNode,
@@ -112,7 +112,7 @@ export function OnboardingFlow({
imports: ImportSourcesController;
onFinish(destination: OnboardingDestination): void;
}) {
const { text, t, locale } = useOnboardingTranslation();
const { text, t } = useOnboardingTranslation();
const [sources, setSources] = useState(imports.sources);
const sourceGeneration = useRef({ value: imports.sources, epoch: 0 });
if (sourceGeneration.current.value !== imports.sources) {
@@ -165,6 +165,7 @@ export function OnboardingFlow({
});
const [scheduleLoaded, setScheduleLoaded] = useState(false);
const [scheduleReadFailed, setScheduleReadFailed] = useState(false);
const [scheduleLoading, setScheduleLoading] = useState(true);
const scheduleReadOperation = useRef(0);
const [modeFocusToken, setModeFocusToken] = useState(
resumedStep === "mode" ? 1 : 0,
@@ -430,7 +431,7 @@ export function OnboardingFlow({
const refreshUltraSchedule = useCallback(async () => {
const operation = ++scheduleReadOperation.current;
setScheduleLoaded(false);
setScheduleReadFailed(false);
setScheduleLoading(true);
try {
const schedule = await getEvolveSchedule();
if (!alive.current || scheduleReadOperation.current !== operation) return;
@@ -446,6 +447,9 @@ export function OnboardingFlow({
setScheduleLoaded(false);
setScheduleReadFailed(true);
}
} finally {
if (alive.current && scheduleReadOperation.current === operation)
setScheduleLoading(false);
}
}, []);
@@ -1260,11 +1264,11 @@ export function OnboardingFlow({
? "正在保存模式…"
: modePhase === "completing"
? "正在完成首次设置…"
: modeChoice === "lite"
? "使用 Lite 进入"
: modeChoice === "pro"
? "继续配置模型"
: "继续",
: !modeChoice
? "继续"
: modeChoice === "lite"
? "使用 Lite 进入"
: "配置模型",
)}
</R6Button>
}
@@ -1274,17 +1278,19 @@ export function OnboardingFlow({
aria-label={text("记忆模式")}
>
<div className="onboarding-r6-mode-intro">
<h2>
{locale === "zh-Hans"
? "Choose how MemHub remembers"
: text("你希望 MemHub 如何形成记忆?")}
</h2>
<h2>{text("你希望 MemHub 如何形成记忆?")}</h2>
<p>{text("需要明确选择一种方式;之后仍可在设置中更改。")}</p>
</div>
<MemoryModeCards
aria-label={text("记忆模式")}
value={modeChoice}
layout="rows"
describedBy={{
ultra:
modeChoice === "ultra"
? "onboarding-ultra-schedule-description"
: undefined,
}}
disabled={modeSaving || completionBusy}
onChange={(mode) => {
modeChoiceTouched.current = true;
@@ -1292,11 +1298,19 @@ export function OnboardingFlow({
setModeChoice(mode);
setModeError(null);
}}
ultraDetail={
scheduleLoaded ? (
<div className="onboarding-r6-ultra-default">
/>
{modeChoice === "ultra" && (
<aside
id="onboarding-ultra-schedule"
className="onboarding-r6-ultra-status"
aria-label={text("每天自动整合")}
aria-busy={scheduleLoading || undefined}
>
<Clock3 size={18} aria-hidden="true" />
{scheduleLoaded ? (
<div className="onboarding-r6-ultra-default" role="status">
<strong>{text("每天自动整合")}</strong>
<small>
<small id="onboarding-ultra-schedule-description">
{text(
ultraSchedule.enabled
? "每天 {time} 左右自动整合,使用这台设备的本地时间;休眠错过后会补跑。"
@@ -1306,21 +1320,32 @@ export function OnboardingFlow({
</div>
) : scheduleReadFailed ? (
<div className="onboarding-r6-schedule-recovery">
<p role="alert">
<p
id="onboarding-ultra-schedule-description"
role="alert"
>
{text("记忆模式状态暂时无法读取,请重试。")}
</p>
<R6Button
className="onboarding-r6-schedule-retry"
pending={scheduleLoading}
disabled={scheduleLoading}
onClick={() => void refreshUltraSchedule()}
>
{text("重试")}
</R6Button>
</div>
) : (
<p>{text("正在读取自动整合设置…")}</p>
)
}
/>
<p
id="onboarding-ultra-schedule-description"
className="onboarding-r6-schedule-loading"
role="status"
>
{text("正在读取自动整合设置…")}
</p>
)}
</aside>
)}
{(modeError || error) && (
<p className="onboarding-r6-mode-error" role="alert">
{text(modeError ?? error ?? "")}
@@ -82,7 +82,7 @@ it("does not reserve an empty eyebrow row", () => {
expect(screen.queryByText("你的记忆,从这里接上")).toBeNull();
});
it("keeps progress cards content-sized and the step-two footer in normal flow", () => {
it("keeps the v1.0.4 step-two split layout and progress geometry", () => {
const card = declaration(
"r6-import-progress.css",
".r6-import-progress-card",
@@ -94,13 +94,12 @@ it("keeps progress cards content-sized and the step-two footer in normal flow",
"first-import-r6.css",
".first-import-r6-main > .r6-import-progress",
),
).toMatch(/flex:\s*0 0 auto/);
expect(declaration("first-import-r6.css", ".first-import-r6-layout")).toMatch(
/height:\s*100%/,
).toMatch(/flex:\s*0 0 218px/);
const layout = declaration("first-import-r6.css", ".first-import-r6-layout");
expect(layout).toMatch(
/grid-template-columns:\s*minmax\(0,\s*738px\)\s+278px/,
);
expect(
declaration("first-import-r6.css", ".first-import-r6-feedback"),
).not.toMatch(/max-height|overflow/);
expect(layout).toMatch(/height:\s*560\.2px/);
expect(declaration("onboarding-r6.css", ".onboarding-r6-body")).toMatch(
/overflow-y:\s*auto/,
);
+49 -189
View File
@@ -1,10 +1,10 @@
/* 143:2419/2502/2594/2685: only step2; shared frame/progress are owned elsewhere. */
.first-import-r6-layout {
display: flex;
flex-direction: column;
gap: 18px;
min-height: 100%;
height: auto;
display: grid;
grid-template-columns: minmax(0, 738px) 278px;
gap: 30px;
height: 560.2px;
min-height: 0;
}
.first-import-r6-main {
min-width: 0;
@@ -13,24 +13,19 @@
flex-direction: column;
}
.first-import-r6-intro {
position: relative;
margin: 0 0 8px;
}
.first-import-r6-intro h2 {
margin: 0 0 3px;
color: #262a2e;
font-size: 16px;
font-weight: 600;
line-height: 26px;
}
.first-import-r6-intro p {
margin: 0;
color: #5a6169;
font-size: 13px;
line-height: 25px;
}
.first-import-r6-subintro {
margin: 1px 0 20px;
color: #63707b;
font-size: 12px;
line-height: 20px;
line-height: 24px;
}
.first-import-r6-history-window {
margin: 0 0 12px;
margin: -10px 0 12px;
padding: 8px 10px;
border: 1px solid #ead7a7;
border-radius: 8px;
@@ -40,114 +35,13 @@
line-height: 20px;
}
.first-import-r6-main > .r6-import-progress {
flex: 0 0 auto;
width: 100%;
margin-inline: auto;
}
.first-import-r6-main > .r6-import-progress[data-count="1"] {
grid-auto-columns: min(320px, 100%);
justify-content: center;
}
.first-import-r6-main > .r6-import-progress[data-count="2"] {
grid-auto-columns: min(320px, calc((100% - 16px) / 2));
justify-content: center;
}
.first-import-r6-main > .r6-import-progress[data-count="3"] {
grid-auto-columns: calc((100% - 32px) / 3);
justify-content: center;
overflow-x: hidden;
}
.first-import-r6-main > .r6-import-progress[data-count="many"] {
grid-auto-flow: column;
grid-auto-columns: calc((100% - 32px) / 3);
justify-content: start;
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x mandatory;
scrollbar-color: rgb(102 128 145 / 50%) transparent;
}
.first-import-r6-main > .r6-import-progress:focus-visible {
outline: 2px solid #557c93;
outline-offset: 3px;
}
.first-import-r6-main > .r6-import-progress .r6-import-progress-card {
min-height: 128px;
padding: 16px 20px;
display: grid;
grid-template-columns: 92px minmax(0, 1fr);
grid-template-rows: auto auto auto;
align-content: center;
column-gap: 18px;
text-align: left;
scroll-snap-align: start;
}
.first-import-r6-main > .r6-import-progress .r6-import-progress-orbit {
width: 86px;
height: 86px;
grid-column: 1;
grid-row: 1 / 4;
margin: 0;
}
.first-import-r6-main > .r6-import-progress .r6-import-progress-icon {
width: 54px;
height: 54px;
border-radius: 14px;
}
.first-import-r6-main > .r6-import-progress :is(h2, p, small) {
grid-column: 2;
}
.first-import-r6-main > .r6-import-progress .r6-import-progress-card h2 {
align-self: end;
}
.first-import-r6-intro .first-import-r6-submission-status {
min-height: 26px;
display: flex;
align-items: center;
gap: 7px;
color: #41697d;
font-weight: 500;
white-space: nowrap;
}
.first-import-r6-submission-status::before {
content: "";
flex: 0 0 auto;
width: 12px;
height: 12px;
border: 1.5px solid rgb(88 124 144 / 28%);
border-top-color: #587c90;
border-radius: 50%;
animation: first-import-r6-checking 1.2s linear infinite;
}
.first-import-r6-intro:has(.first-import-r6-submission-status) {
display: flex;
align-items: center;
gap: 12px;
}
.first-import-r6-intro:has(.first-import-r6-submission-status) h2 {
flex: 0 0 auto;
margin: 0;
}
.first-import-r6-intro .first-import-r6-scroll-hint {
position: absolute;
right: 0;
bottom: 0;
color: #6b7e8b;
font-size: 11px;
line-height: 20px;
white-space: nowrap;
}
.first-import-r6-intro:has(.first-import-r6-scroll-hint) {
padding-right: 190px;
}
@keyframes first-import-r6-checking {
to {
transform: rotate(360deg);
}
flex: 0 0 218px;
}
.first-import-r6-feed {
flex: 0 0 auto;
margin-top: 20px;
overflow: visible;
min-height: 0;
flex: 1;
margin-top: 26px;
overflow: auto;
scrollbar-width: thin;
overscroll-behavior: contain;
}
@@ -194,14 +88,9 @@
margin: 0;
}
.first-import-r6-options {
flex: 1 1 auto;
min-width: 0;
min-height: 132px;
padding: 22px 24px;
display: grid;
grid-template-columns: minmax(190px, 0.7fr) minmax(0, 1.3fr);
align-items: center;
gap: 0 28px;
min-height: 0;
padding: 24px;
border: 1px solid #ffffffe0;
border-radius: 22px;
background:
@@ -210,13 +99,9 @@
box-shadow:
inset 0 1px #fff,
0 12px 32px rgb(49 75 101 / 4%);
overflow: visible;
overflow: auto;
scrollbar-width: thin;
overscroll-behavior: contain;
align-self: stretch;
}
.first-import-r6-options-head {
min-width: 0;
}
.first-import-r6-eyebrow {
margin: 0 0 14px;
@@ -243,12 +128,12 @@
position: relative;
display: block;
width: 100%;
min-height: 104px;
margin: 0;
padding: 18px 52px 16px 18px;
min-height: 124px;
margin: 0 0 16px;
padding: 18px 16px 12px;
border: 1px solid rgb(179 196 209 / 42%);
border-radius: 12px;
background: rgb(255 255 255 / 34%);
border-radius: 0;
background: rgb(255 255 255 / 10%);
box-shadow: inset 0 1px 1px #ffffffd9;
backdrop-filter: blur(20px);
cursor: pointer;
@@ -277,8 +162,8 @@
.first-import-r6-selection input {
appearance: none;
position: absolute;
top: 20px;
right: 20px;
top: 22px;
right: 18px;
width: 16px;
height: 16px;
min-height: 16px;
@@ -316,11 +201,10 @@
display: block;
font-size: 12px;
line-height: 20px;
margin-top: 7px;
margin-top: 8px;
color: #5e6e7a;
}
.first-import-r6-options-note {
grid-column: 1 / -1;
font-size: 11px;
line-height: 22px;
margin: 0 0 12px;
@@ -340,37 +224,33 @@
.first-import-r6-options details p {
margin: 6px 0;
}
.first-import-r6-feedback {
margin: 0 0 12px;
padding: 10px 12px;
border-radius: 8px;
background: rgb(255 255 255 / 50%);
.first-import-r6-footer-note {
flex: 1;
min-width: 0;
max-height: 64px;
overflow: auto;
}
.first-import-r6-feedback p {
.first-import-r6-footer-note p {
margin: 0;
font-size: 12px;
line-height: 20px;
color: #63707b;
}
.first-import-r6-feedback [role="alert"] {
.first-import-r6-footer-note [role="alert"] {
color: #944242;
}
.first-import-r6-feedback .first-import-r6-partial-success {
.first-import-r6-footer-note .first-import-r6-partial-success {
color: #245f50;
font-weight: 600;
}
.first-import-r6-footer-actions {
display: flex;
flex: 0 1 auto;
min-width: 0;
max-width: 100%;
flex-wrap: wrap;
gap: 12px;
gap: 16px;
align-items: center;
margin-left: auto;
}
.first-import-r6-footer-actions > [data-variant="primary"] {
min-width: min(160px, 100%);
min-width: 210px;
}
.first-import-r6-footer-actions .r6-button-label {
display: inline-flex;
@@ -517,28 +397,13 @@ dialog.first-import-r6-original[open] {
}
@media (max-width: 1150px) {
.first-import-r6-layout {
gap: 18px;
}
}
@media (min-width: 761px) and (max-height: 859px) {
.onboarding-r6:has(.first-import-r6-layout)
> :is(.onboarding-r6-titlebar, .onboarding-r6-local-footer) {
display: none;
}
.onboarding-r6:has(.first-import-r6-layout) > .onboarding-r6-stage {
min-height: 0;
height: 100dvh;
padding: 10px 24px 2px;
}
.onboarding-r6:has(.first-import-r6-layout)
> .onboarding-r6-stage
> .onboarding-r6-shell {
height: 100%;
grid-template-columns: minmax(0, 1fr) 278px;
gap: 24px;
}
}
@media (min-width: 761px) and (max-height: 959px) {
.onboarding-r6-body:has(> .first-import-r6-layout) {
overflow-y: auto;
overflow: hidden;
padding-top: 16px;
padding-bottom: 16px;
}
@@ -548,8 +413,8 @@ dialog.first-import-r6-original[open] {
}
@media (min-width: 761px) and (max-height: 859px) {
.first-import-r6-options {
padding: 18px 20px;
overflow: visible;
padding: 14px;
overflow: hidden;
}
.first-import-r6-eyebrow {
margin-bottom: 4px;
@@ -566,16 +431,16 @@ dialog.first-import-r6-original[open] {
}
.first-import-r6-selection,
.first-import-r6-selection + .first-import-r6-selection {
min-height: 96px;
margin-bottom: 0;
padding: 15px 48px 13px 16px;
min-height: 86px;
margin-bottom: 8px;
padding: 9px 12px;
}
.first-import-r6-selection strong {
line-height: 24px;
}
.first-import-r6-selection input {
top: 17px;
right: 16px;
top: 13px;
right: 12px;
}
.first-import-r6-selection span {
margin-top: 4px;
@@ -596,7 +461,6 @@ dialog.first-import-r6-original[open] {
overflow: visible;
}
.first-import-r6-options {
display: block;
overflow: visible;
}
.first-import-r6-footer-note {
@@ -654,10 +518,6 @@ dialog.first-import-r6-original[open] {
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
.first-import-r6-submission-status::before {
animation: none;
border-color: #587c90;
}
.first-import-r6-selection,
.first-import-r6-selection input::after {
transition: none;
+12 -12
View File
@@ -300,40 +300,40 @@
@media (min-width: 761px) and (max-height: 959px) {
.memory-mode-cards--rows {
min-height: 360px;
min-height: 284px;
}
.memory-mode-cards--rows .memory-mode-card-list {
gap: 12px;
gap: 10px;
}
.memory-mode-cards--rows .memory-mode-card {
min-height: 112px;
min-height: 88px;
}
.memory-mode-cards--rows .memory-mode-card-choice {
min-height: 0;
padding: 13px 16px;
padding: 9px 16px;
grid-template-columns: minmax(205px, 0.82fr) minmax(0, 1.5fr);
column-gap: 22px;
}
.memory-mode-cards--rows .memory-mode-radio {
top: 13px;
top: 9px;
right: 16px;
}
.memory-mode-cards--rows .memory-mode-title strong {
font-size: 17px;
line-height: 25px;
font-size: 16px;
line-height: 22px;
}
.memory-mode-cards--rows .memory-mode-description {
min-height: 0;
margin-top: 4px;
font-size: 12px;
line-height: 18px;
margin-top: 2px;
font-size: 11px;
line-height: 16px;
}
.memory-mode-cards--rows .memory-mode-features {
margin-top: 0;
padding: 0 36px 0 22px;
gap: 6px;
gap: 3px;
font-size: 11px;
line-height: 17px;
line-height: 15px;
}
.memory-mode-cards--rows .memory-mode-detail {
margin-top: 5px;
+68 -3
View File
@@ -588,6 +588,52 @@
font-size: 11px;
line-height: 18px;
}
.onboarding-r6-ultra-status {
min-height: 66px;
padding: 12px 14px;
display: grid;
grid-template-columns: 22px minmax(0, 1fr);
align-items: center;
gap: 12px;
border: 1px solid rgb(143 169 158 / 42%);
border-radius: 12px;
background:
linear-gradient(105deg, rgb(236 244 240 / 88%), rgb(255 255 255 / 56%)),
rgb(255 255 255 / 42%);
box-shadow:
inset 0 1px rgb(255 255 255 / 82%),
0 8px 24px rgb(47 107 87 / 5%);
color: #405867;
animation: onboarding-r6-ultra-status-in 180ms cubic-bezier(0.16, 1, 0.3, 1)
both;
}
.onboarding-r6-ultra-status > svg {
color: #527565;
}
.onboarding-r6-schedule-loading {
margin: 0;
color: #607482;
font-size: 12px;
line-height: 20px;
}
.onboarding-r6-ultra-status .onboarding-r6-schedule-recovery {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
text-align: left;
}
.onboarding-r6-ultra-status .onboarding-r6-schedule-retry {
flex: 0 0 auto;
min-width: 76px;
}
@keyframes onboarding-r6-ultra-status-in {
from {
opacity: 0;
transform: translateY(-4px);
}
}
.onboarding-r6-evolve-settings {
margin-top: 0;
padding: 18px 20px 4px;
@@ -615,7 +661,7 @@
font-weight: 600;
line-height: 22px;
}
.onboarding-r6-schedule-recovery {
.onboarding-r6-mode .onboarding-r6-schedule-recovery {
display: grid;
justify-items: center;
gap: 10px;
@@ -623,7 +669,7 @@
font-size: 13px;
line-height: 20px;
}
.onboarding-r6-schedule-recovery p {
.onboarding-r6-mode .onboarding-r6-schedule-recovery p {
margin: 0;
}
.onboarding-r6-mode-error {
@@ -637,7 +683,23 @@
@media (min-width: 761px) {
.onboarding-r6-mode {
min-height: 100%;
gap: 24px;
}
}
@media (min-width: 761px) and (max-height: 959px) {
.onboarding-r6-mode {
gap: 12px;
}
.onboarding-r6-mode-intro h2 {
font-size: 20px;
line-height: 28px;
}
.onboarding-r6-mode-intro p {
font-size: 12px;
line-height: 20px;
}
.onboarding-r6-ultra-status {
min-height: 58px;
padding-block: 9px;
}
}
.onboarding-r6-empty {
@@ -980,6 +1042,9 @@
.onboarding-r6-badge[data-found="true"] i {
animation: none;
}
.onboarding-r6-ultra-status {
animation: none;
}
.onboarding-r6-row,
.onboarding-r6 .onboarding-r6-quiet,
.onboarding-r6-list-status {
+1 -1
View File
@@ -23,7 +23,7 @@ const modeChineseMessages = {
"当前模型已经启用,可以继续完成首次设置。":
"当前模型已经启用,可以继续完成首次设置。",
继续使用当前模型: "继续使用当前模型",
"你希望 MemHub 如何形成记忆?": "选择记忆模式",
"你希望 MemHub 如何形成记忆?": "你希望 MemHub 如何形成记忆?",
"需要明确选择一种方式;之后仍可在设置中更改。": "之后可在设置中更改。",
记忆模式: "记忆模式",
记忆模式与模型: "记忆模式与模型",