Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8c833142a |
No files matched your search
@@ -371,13 +371,11 @@ it("uses user-facing connection confirmation copy without implementation narrati
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole("complementary", { name: "连接应用" })).toBeTruthy();
|
||||
expect(screen.getByRole("checkbox", { name: "一键接入应用" })).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"点击底部按钮后,后台分别接入各应用;某个应用失败不影响其他应用。",
|
||||
screen.queryByText(
|
||||
/点击底部按钮后|选择草稿|按各应用回执|本页任务控制尚未接通/,
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
screen.queryByText(/选择草稿|按各应用回执|本页任务控制尚未接通/),
|
||||
).toBeNull();
|
||||
expect(screen.queryByText(/下一步选择记忆模式/)).toBeNull();
|
||||
expect(screen.queryByText(/之后仍可在设置中更改/)).toBeNull();
|
||||
|
||||
@@ -148,6 +148,11 @@ export function FirstImportReview({
|
||||
const canResume = batch.some((job) =>
|
||||
["paused", "interrupted", "failed"].includes(job.status),
|
||||
);
|
||||
const allCompleted =
|
||||
batch.length > 0 &&
|
||||
ids.every((id) => batch.some((job) => job.source_id === id)) &&
|
||||
batch.every((job) => job.status === "completed") &&
|
||||
!Object.keys(failedSources).length;
|
||||
const title = "对话正在汇入,记忆从这里开始";
|
||||
const hostEligible = importHostEligible(sources, ids);
|
||||
const draftReady = !!connectionDraft && !!onConnectionDraftChange;
|
||||
@@ -322,6 +327,16 @@ export function FirstImportReview({
|
||||
className="first-import-r6-main"
|
||||
aria-label={text("导入与原文")}
|
||||
>
|
||||
<div className="first-import-r6-intro">
|
||||
<h2>{text("正在导入你的会话")}</h2>
|
||||
<p>
|
||||
{text(
|
||||
allCompleted
|
||||
? "本次导入已结束。已保存的对话无需重复导入。"
|
||||
: "后台将持续为你导入,可以放心进入下一步",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{(error || actionMessage) && (
|
||||
<div className="first-import-r6-feedback">
|
||||
<p
|
||||
@@ -355,46 +370,39 @@ export function FirstImportReview({
|
||||
jobs={batch}
|
||||
stale={progressStale}
|
||||
/>
|
||||
<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("未开始的导入")}
|
||||
{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")}
|
||||
>
|
||||
{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>
|
||||
{text("查看未导入的应用")}
|
||||
</R6Button>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
<aside
|
||||
className="first-import-r6-options"
|
||||
aria-label={text("连接应用")}
|
||||
>
|
||||
<p className="first-import-r6-eyebrow">{text("让记忆继续生长")}</p>
|
||||
<h2>{text("连接你的应用")}</h2>
|
||||
<p className="first-import-r6-options-intro">
|
||||
{text("适用于本次选择且支持的应用。")}
|
||||
</p>
|
||||
<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>
|
||||
<SelectionTile
|
||||
title={text(
|
||||
hostConnectionAvailable ? "一键接入应用" : "测试实例不接入应用",
|
||||
@@ -430,13 +438,11 @@ export function FirstImportReview({
|
||||
{text(connectionNote)}
|
||||
</p>
|
||||
)}
|
||||
<p className="first-import-r6-options-note">
|
||||
{text(
|
||||
draftReady
|
||||
? "点击底部按钮后,后台分别接入各应用;某个应用失败不影响其他应用。"
|
||||
: "暂时无法连接应用,当前不会安装连接。",
|
||||
)}
|
||||
</p>
|
||||
{!draftReady && (
|
||||
<p className="first-import-r6-options-note">
|
||||
{text("暂时无法连接应用,当前不会安装连接。")}
|
||||
</p>
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
</R6ColdStartFrame>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import type { MemoryPipelineMode } from "@one-memory/contracts";
|
||||
import { Check } from "lucide-react";
|
||||
import { type ReactNode, useId } from "react";
|
||||
import { MODE_COPY } from "./memory-mode-ui";
|
||||
import { useOnboardingTranslation } from "./onboarding-language";
|
||||
import "./memory-mode-cards.css";
|
||||
|
||||
const MODES: Array<{
|
||||
id: MemoryPipelineMode;
|
||||
index: string;
|
||||
description: string;
|
||||
}> = [
|
||||
{
|
||||
id: "lite",
|
||||
index: "01",
|
||||
description: "不调用生成模型。保存并检索原始对话,不再生成高级记忆。",
|
||||
},
|
||||
{
|
||||
id: "pro",
|
||||
index: "02",
|
||||
description: "使用当前模型从新对话中提取事实、偏好和计划。",
|
||||
},
|
||||
{
|
||||
id: "ultra",
|
||||
index: "03",
|
||||
description: "在智能记忆之上使用知识图谱,并默认每天 03:00 自动整合记忆。",
|
||||
},
|
||||
];
|
||||
|
||||
export function MemoryModeCards({
|
||||
value,
|
||||
current,
|
||||
currentBadge,
|
||||
disabled = false,
|
||||
onChange,
|
||||
ultraDetail,
|
||||
"aria-label": ariaLabel,
|
||||
"aria-busy": ariaBusy,
|
||||
}: {
|
||||
value: MemoryPipelineMode | null;
|
||||
current?: MemoryPipelineMode;
|
||||
currentBadge?: ReactNode;
|
||||
disabled?: boolean | ((mode: MemoryPipelineMode) => boolean);
|
||||
onChange(mode: MemoryPipelineMode, control: HTMLInputElement): void;
|
||||
ultraDetail?: ReactNode;
|
||||
"aria-label": string;
|
||||
"aria-busy"?: boolean;
|
||||
}) {
|
||||
const { text } = useOnboardingTranslation();
|
||||
const name = useId();
|
||||
const unavailable = (mode: MemoryPipelineMode) =>
|
||||
typeof disabled === "function" ? disabled(mode) : disabled;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="memory-mode-cards"
|
||||
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)}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</label>
|
||||
{mode.id === "ultra" && selected && ultraDetail ? (
|
||||
<div className="memory-mode-detail">{ultraDetail}</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -96,7 +96,7 @@ it("renders all settings modes as one grouped shared choice list", async () => {
|
||||
expect(within(group).getAllByRole("radio")).toHaveLength(3);
|
||||
expect(
|
||||
within(group).getByText("当前模式").closest("label")?.textContent,
|
||||
).toContain("智能记忆(pro)");
|
||||
).toContain("智能记忆PRO");
|
||||
});
|
||||
|
||||
it("confirms the data effects before switching to lite", async () => {
|
||||
|
||||
@@ -25,12 +25,8 @@ import {
|
||||
commitConfirmedMemoryMode,
|
||||
issueConfirmedModeTicket,
|
||||
} from "./confirmed-memory-mode";
|
||||
import { MemoryModeChoiceList } from "./MemoryModeChoiceList";
|
||||
import {
|
||||
BLOCKER_COPY,
|
||||
MemoryModeConfirmDialog,
|
||||
MODE_COPY,
|
||||
} from "./memory-mode-ui";
|
||||
import { MemoryModeCards } from "./MemoryModeCards";
|
||||
import { BLOCKER_COPY, MemoryModeConfirmDialog } from "./memory-mode-ui";
|
||||
import { useOnboardingTranslation } from "./onboarding-language";
|
||||
import { R6Button } from "./R6Controls";
|
||||
import { R6StateRecovery } from "./R6StateRecovery";
|
||||
@@ -237,16 +233,11 @@ export function MemoryModeSettings({
|
||||
<p>{text("模式决定新对话如何保存,以及记忆页面展示什么。")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<MemoryModeChoiceList
|
||||
<MemoryModeCards
|
||||
aria-label={text("当前模式")}
|
||||
aria-busy={state === null}
|
||||
modes={(["lite", "pro", "ultra"] as const).map((mode) => ({
|
||||
mode,
|
||||
title: text(MODE_COPY[mode].title),
|
||||
description: text(MODE_COPY[mode].description),
|
||||
}))}
|
||||
current={state?.mode}
|
||||
selected={state?.mode ?? null}
|
||||
value={state?.mode ?? null}
|
||||
currentBadge={text("当前模式")}
|
||||
disabled={(mode) =>
|
||||
!state ||
|
||||
@@ -256,6 +247,50 @@ export function MemoryModeSettings({
|
||||
(mode !== "lite" && !modelStateKnown)
|
||||
}
|
||||
onChange={(mode, control) => void choose(mode, control)}
|
||||
ultraDetail={
|
||||
state?.mode === "ultra" && schedule ? (
|
||||
<section
|
||||
id={`${id}-evolve`}
|
||||
className="r6-evolve-settings r6-evolve-settings--in-card"
|
||||
aria-labelledby={`${id}-evolve-title`}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<UltraEvolveScheduleFields
|
||||
schedule={schedule}
|
||||
headingId={`${id}-evolve-title`}
|
||||
disabled={busy || maintenance}
|
||||
onEnabledChange={(enabled) => void changeSchedule({ enabled })}
|
||||
onTimeChange={(time) => changeSchedule({ time })}
|
||||
/>
|
||||
<div className="r6-evolve-setting r6-evolve-setting--manual">
|
||||
<div className="r6-evolve-setting-copy">
|
||||
<h3>{text("立即整合一次")}</h3>
|
||||
<p>
|
||||
{text("只运行这一次,不改变每天的计划。")}
|
||||
{" · "}
|
||||
{text(
|
||||
run?.state === "running"
|
||||
? "正在整合记忆…"
|
||||
: last?.last_run_at
|
||||
? text("上次整合:{time}").replace(
|
||||
"{time}",
|
||||
new Date(last.last_run_at).toLocaleString(locale),
|
||||
)
|
||||
: "尚未整合",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<R6Button
|
||||
pending={run?.state === "running"}
|
||||
disabled={busy || lifecycleBusy}
|
||||
onClick={() => void evolveNow()}
|
||||
>
|
||||
{text("立即整合")}
|
||||
</R6Button>
|
||||
</div>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
{state?.forced_by_env ? (
|
||||
<p className="r6-settings-note" role="status">
|
||||
@@ -285,49 +320,6 @@ export function MemoryModeSettings({
|
||||
)
|
||||
) : null}
|
||||
|
||||
{state?.mode === "ultra" && schedule ? (
|
||||
<section
|
||||
id={`${id}-evolve`}
|
||||
className="r6-evolve-settings"
|
||||
aria-labelledby={`${id}-evolve-title`}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<UltraEvolveScheduleFields
|
||||
schedule={schedule}
|
||||
headingId={`${id}-evolve-title`}
|
||||
disabled={busy || maintenance}
|
||||
onEnabledChange={(enabled) => void changeSchedule({ enabled })}
|
||||
onTimeChange={(time) => changeSchedule({ time })}
|
||||
/>
|
||||
<div className="r6-evolve-setting r6-evolve-setting--manual">
|
||||
<div className="r6-evolve-setting-copy">
|
||||
<h3>{text("立即整合一次")}</h3>
|
||||
<p>
|
||||
{text("只运行这一次,不改变每天的计划。")}
|
||||
{" · "}
|
||||
{text(
|
||||
run?.state === "running"
|
||||
? "正在整合记忆…"
|
||||
: last?.last_run_at
|
||||
? text("上次整合:{time}").replace(
|
||||
"{time}",
|
||||
new Date(last.last_run_at).toLocaleString(locale),
|
||||
)
|
||||
: "尚未整合",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<R6Button
|
||||
pending={run?.state === "running"}
|
||||
disabled={busy || lifecycleBusy}
|
||||
onClick={() => void evolveNow()}
|
||||
>
|
||||
{text("立即整合")}
|
||||
</R6Button>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{pendingMode && pendingPlan ? (
|
||||
<MemoryModeConfirmDialog
|
||||
mode={pendingMode}
|
||||
|
||||
@@ -23,20 +23,20 @@ vi.mock("./api", () => ({
|
||||
explicitly_configured: false,
|
||||
}),
|
||||
setMemoryMode: vi.fn().mockResolvedValue({ mode: "pro" }),
|
||||
planMemoryMode: vi.fn().mockResolvedValue({
|
||||
planMemoryMode: vi.fn().mockImplementation(async (target) => ({
|
||||
current: "lite",
|
||||
target: "pro",
|
||||
effective_target: "pro",
|
||||
target,
|
||||
effective_target: target,
|
||||
capabilities: {
|
||||
content_plane: "memory",
|
||||
generation_required: true,
|
||||
graph_enabled: false,
|
||||
evolve_available: false,
|
||||
content_plane: target === "lite" ? "raw" : "memory",
|
||||
generation_required: target !== "lite",
|
||||
graph_enabled: target === "ultra",
|
||||
evolve_available: target === "ultra",
|
||||
},
|
||||
effects: [],
|
||||
warnings: [],
|
||||
blockers: ["active_model_required"],
|
||||
}),
|
||||
blockers: target === "lite" ? [] : ["active_model_required"],
|
||||
})),
|
||||
putOnboardingProgress: vi.fn().mockResolvedValue({ model_profile_id: null }),
|
||||
patchOnboardingProgress: vi.fn().mockResolvedValue({}),
|
||||
}));
|
||||
@@ -156,9 +156,6 @@ 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(
|
||||
await screen.findByRole("button", { name: "确认选择" }),
|
||||
);
|
||||
await userEvent.click(screen.getByRole("button", { name: "继续配置模型" }));
|
||||
expect(screen.getByRole("heading", { name: "第三步模型页面" })).toBeTruthy();
|
||||
expect(completeOnboarding).not.toHaveBeenCalled();
|
||||
@@ -187,9 +184,6 @@ it("lets an empty first-time user enter without granting or importing a source",
|
||||
await waitFor(() => expect((skip as HTMLButtonElement).disabled).toBe(false));
|
||||
await userEvent.click(skip);
|
||||
await userEvent.click(screen.getByRole("radio", { name: /lite/i }));
|
||||
await userEvent.click(
|
||||
await screen.findByRole("button", { name: "确认选择" }),
|
||||
);
|
||||
await userEvent.click(screen.getByRole("button", { name: "使用 Lite 进入" }));
|
||||
await waitFor(() =>
|
||||
expect(finish).toHaveBeenCalledExactlyOnceWith("sources"),
|
||||
@@ -213,9 +207,6 @@ it("retains explicit retry or enter when saving the skipped onboarding fails", a
|
||||
await waitFor(() => expect((skip as HTMLButtonElement).disabled).toBe(false));
|
||||
await userEvent.click(skip);
|
||||
await userEvent.click(screen.getByRole("radio", { name: /lite/i }));
|
||||
await userEvent.click(
|
||||
await screen.findByRole("button", { name: "确认选择" }),
|
||||
);
|
||||
await userEvent.click(screen.getByRole("button", { name: "使用 Lite 进入" }));
|
||||
await screen.findByRole("button", { name: "重试保存并进入" });
|
||||
expect(finish).not.toHaveBeenCalled();
|
||||
|
||||
@@ -329,7 +329,6 @@ it("uses compact Spanish actions and preserves Chinese copy and permissions", as
|
||||
expect(
|
||||
screen.getByText(/Solo para las apps seleccionadas y compatibles/),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText(/Si una falla, las demás siguen/)).toBeTruthy();
|
||||
fireEvent.click(screen.getByText("中文"));
|
||||
expect(screen.getByRole("button", { name: "选择记忆模式" })).toBeTruthy();
|
||||
expect(navigation.textContent).toContain("连接应用");
|
||||
@@ -447,9 +446,6 @@ it("translates completion recovery live without repeating completion writes", as
|
||||
screen.getByRole("button", { name: "Elegir el modo de memoria" }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("radio", { name: /lite/i }));
|
||||
fireEvent.click(
|
||||
await screen.findByRole("button", { name: "Confirmar selección" }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Entrar con Lite" }));
|
||||
expect(
|
||||
await screen.findByRole("button", { name: "Reintentar guardar y entrar" }),
|
||||
|
||||
@@ -353,7 +353,6 @@ it("empty discovery shows zero and allows explicit entry without a connection re
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "选择记忆模式" }));
|
||||
fireEvent.click(screen.getByRole("radio", { name: /lite/i }));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "确认选择" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "使用 Lite 进入" }));
|
||||
await waitFor(() =>
|
||||
expect(finish).toHaveBeenCalledExactlyOnceWith("sources"),
|
||||
@@ -400,7 +399,6 @@ it("partial acceptance preserves the successful job and failure through model-st
|
||||
fireEvent.click(screen.getByRole("button", { name: "继续配置模型" }));
|
||||
expect(api.completeOnboarding).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole("radio", { name: /lite/i }));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "确认选择" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "使用 Lite 进入" }));
|
||||
await waitFor(() =>
|
||||
expect(finish).toHaveBeenCalledExactlyOnceWith("sources"),
|
||||
|
||||
@@ -251,7 +251,6 @@ async function selectMode(
|
||||
name: RegExp,
|
||||
) {
|
||||
await user.click(screen.getByRole("radio", { name }));
|
||||
await user.click(await screen.findByRole("button", { name: "确认选择" }));
|
||||
}
|
||||
function choice(name = "Claude Code") {
|
||||
return screen.getByRole<HTMLInputElement>("checkbox", {
|
||||
@@ -948,7 +947,7 @@ describe("OnboardingFlow S1.3", () => {
|
||||
await user.click(screen.getByRole("button", { name: SKIP }));
|
||||
|
||||
expect(screen.getByRole("heading", { name: "选择记忆模式" })).toBeTruthy();
|
||||
expect(screen.queryByText("之后可在设置中更改。")).toBeNull();
|
||||
expect(screen.getByText("之后可在设置中更改。")).toBeTruthy();
|
||||
expect(screen.getAllByRole("radio")).toHaveLength(3);
|
||||
expect(screen.getByRole("radio", { name: /ultra/i })).toBeTruthy();
|
||||
expect(screen.queryByText("推荐")).toBeNull();
|
||||
@@ -1001,24 +1000,8 @@ describe("OnboardingFlow S1.3", () => {
|
||||
await openApps();
|
||||
await user.click(screen.getByRole("button", { name: SKIP }));
|
||||
await user.click(screen.getByRole("radio", { name: /ultra/i }));
|
||||
expect(
|
||||
await screen.findByRole("heading", {
|
||||
name: "选择深度记忆(ultra)?",
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText("使用 pro 或 ultra 前需要先配置并启用生成模型。"),
|
||||
).toBeTruthy();
|
||||
await user.click(screen.getByRole("button", { name: "确认选择" }));
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getByRole("switch", { name: "每天自动整合" })
|
||||
.getAttribute("aria-checked"),
|
||||
).toBe("true");
|
||||
expect(
|
||||
screen.getByRole("button", { name: "执行时间,03:00" }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText("每天自动整合")).toBeTruthy();
|
||||
expect(screen.getByText(/每天 03:00 左右自动整合/)).toBeTruthy();
|
||||
await user.click(screen.getByRole("button", { name: "继续" }));
|
||||
expect(screen.getByRole("region", { name: "模型步骤边界" })).toBeTruthy();
|
||||
expect(putOnboardingProgress).toHaveBeenCalledWith(
|
||||
@@ -1087,11 +1070,7 @@ describe("OnboardingFlow S1.3", () => {
|
||||
false,
|
||||
),
|
||||
);
|
||||
expect(
|
||||
screen
|
||||
.getByRole("switch", { name: "每天自动整合" })
|
||||
.getAttribute("aria-checked"),
|
||||
).toBe("false");
|
||||
expect(screen.getByText(/重新开启后仍按 05:30 运行/)).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "继续" }));
|
||||
await waitFor(() =>
|
||||
@@ -1169,9 +1148,7 @@ describe("OnboardingFlow S1.3", () => {
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "重试" }));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole("button", { name: "执行时间,04:15" }),
|
||||
).toBeTruthy(),
|
||||
expect(screen.getByText(/每天 04:15 左右自动整合/)).toBeTruthy(),
|
||||
);
|
||||
expect(
|
||||
screen.getByRole<HTMLInputElement>("radio", { name: /ultra/i }).checked,
|
||||
|
||||
@@ -2,8 +2,6 @@ import type {
|
||||
ConnectSourcesResponse,
|
||||
EvolveSchedule,
|
||||
ImportJob,
|
||||
MemoryModePlan,
|
||||
MemoryPipelineMode,
|
||||
OnboardingModeIntent,
|
||||
OnboardingState,
|
||||
SourceResponse,
|
||||
@@ -49,17 +47,12 @@ import {
|
||||
hostCapabilities,
|
||||
hostWriteReason,
|
||||
} from "./host-state";
|
||||
import { MemoryModeChoiceList } from "./MemoryModeChoiceList";
|
||||
import {
|
||||
BLOCKER_COPY,
|
||||
MemoryModeConfirmDialog,
|
||||
MODE_COPY,
|
||||
} from "./memory-mode-ui";
|
||||
import { MemoryModeCards } from "./MemoryModeCards";
|
||||
import { BLOCKER_COPY } from "./memory-mode-ui";
|
||||
import { OnboardingModelController } from "./OnboardingModelController";
|
||||
import { useOnboardingTranslation } from "./onboarding-language";
|
||||
import { R6ColdStartFrame } from "./R6ColdStartFrame";
|
||||
import { R6Button } from "./R6Controls";
|
||||
import { UltraEvolveScheduleFields } from "./UltraEvolveScheduleFields";
|
||||
import { traceUiAction } from "./ui-trace";
|
||||
import type { ImportSourcesController } from "./use-import-sources";
|
||||
import { isDefiniteRefusal } from "./write-outcome";
|
||||
@@ -115,7 +108,7 @@ export function OnboardingFlow({
|
||||
imports: ImportSourcesController;
|
||||
onFinish(destination: OnboardingDestination): void;
|
||||
}) {
|
||||
const { text, t } = useOnboardingTranslation();
|
||||
const { text, t, locale } = useOnboardingTranslation();
|
||||
const [sources, setSources] = useState(imports.sources);
|
||||
const sourceGeneration = useRef({ value: imports.sources, epoch: 0 });
|
||||
if (sourceGeneration.current.value !== imports.sources) {
|
||||
@@ -159,14 +152,7 @@ export function OnboardingFlow({
|
||||
);
|
||||
const modeSaving = modePhase !== "idle";
|
||||
const [modeError, setModeError] = useState<string | null>(null);
|
||||
const [knownMode, setKnownMode] = useState<MemoryPipelineMode>("pro");
|
||||
const [pendingMode, setPendingMode] = useState<OnboardingMemoryMode | null>(
|
||||
null,
|
||||
);
|
||||
const [pendingPlan, setPendingPlan] = useState<MemoryModePlan | null>(null);
|
||||
const [modePlanBusy, setModePlanBusy] = useState(false);
|
||||
const modeDialogOpener = useRef<HTMLElement | null>(null);
|
||||
const modePlanOperation = useRef(0);
|
||||
const [knownMode, setKnownMode] = useState<OnboardingMemoryMode>("pro");
|
||||
const [ultraSchedule, setUltraSchedule] = useState<EvolveSchedule>({
|
||||
enabled: true,
|
||||
time: "03:00",
|
||||
@@ -704,10 +690,6 @@ export function OnboardingFlow({
|
||||
const requestModeChoice = useCallback(
|
||||
(destination: OnboardingDestination) => {
|
||||
if (finished.current) return;
|
||||
modePlanOperation.current += 1;
|
||||
setModePlanBusy(false);
|
||||
setPendingMode(null);
|
||||
setPendingPlan(null);
|
||||
if (setupStepRef.current === "model")
|
||||
leaveModelStep(intentRef.current === "ultra" ? "ultra" : "pro", "mode");
|
||||
else {
|
||||
@@ -724,45 +706,19 @@ export function OnboardingFlow({
|
||||
[leaveModelStep, modeChoice],
|
||||
);
|
||||
|
||||
const requestModeSelection = useCallback(
|
||||
async (mode: OnboardingMemoryMode, opener: HTMLElement) => {
|
||||
if (modePlanBusy || modeSaving || completionBusy) return;
|
||||
const operation = ++modePlanOperation.current;
|
||||
setModePlanBusy(true);
|
||||
setModeError(null);
|
||||
const loadModePlan = useCallback(
|
||||
async (target: OnboardingMemoryMode) => {
|
||||
try {
|
||||
let plan: MemoryModePlan;
|
||||
try {
|
||||
plan = await planMemoryMode(mode);
|
||||
} catch (reason) {
|
||||
if (!memoryModePlanUnsupported(reason)) throw reason;
|
||||
const profiles = await getModelProfiles();
|
||||
plan = legacyMemoryModePlan(knownMode, mode, {
|
||||
hasActiveModel: profiles.some((profile) => profile.is_active),
|
||||
});
|
||||
}
|
||||
if (
|
||||
!alive.current ||
|
||||
modePlanOperation.current !== operation ||
|
||||
setupStepRef.current !== "mode"
|
||||
)
|
||||
return;
|
||||
modeDialogOpener.current = opener;
|
||||
setPendingMode(mode);
|
||||
setPendingPlan(plan);
|
||||
} catch {
|
||||
if (
|
||||
alive.current &&
|
||||
modePlanOperation.current === operation &&
|
||||
setupStepRef.current === "mode"
|
||||
)
|
||||
setModeError("模式切换影响暂时无法读取,请重试。");
|
||||
} finally {
|
||||
if (alive.current && modePlanOperation.current === operation)
|
||||
setModePlanBusy(false);
|
||||
return await planMemoryMode(target);
|
||||
} catch (reason) {
|
||||
if (!memoryModePlanUnsupported(reason)) throw reason;
|
||||
const profiles = await getModelProfiles();
|
||||
return legacyMemoryModePlan(knownMode, target, {
|
||||
hasActiveModel: profiles.some((profile) => profile.is_active),
|
||||
});
|
||||
}
|
||||
},
|
||||
[completionBusy, knownMode, modePlanBusy, modeSaving],
|
||||
[knownMode],
|
||||
);
|
||||
|
||||
const saveModeChoice = useCallback(async () => {
|
||||
@@ -777,7 +733,7 @@ export function OnboardingFlow({
|
||||
if (modeChoice !== "lite") {
|
||||
const target = modeChoice;
|
||||
const [plan, profiles] = await Promise.all([
|
||||
planMemoryMode(target),
|
||||
loadModePlan(target),
|
||||
getModelProfiles(),
|
||||
]);
|
||||
if (!ownsModeOperation()) return;
|
||||
@@ -856,6 +812,14 @@ export function OnboardingFlow({
|
||||
ownsModeOperation() &&
|
||||
intentRef.current === "lite" &&
|
||||
setupStepRef.current === "mode";
|
||||
const plan = await loadModePlan("lite");
|
||||
if (!liteStillCurrent()) return;
|
||||
if (plan.blockers.length > 0) {
|
||||
setModeError(
|
||||
plan.blockers.map((blocker) => BLOCKER_COPY[blocker]).join(" "),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const changed = await queueModeWrite("lite", liteStillCurrent);
|
||||
if (!liteStillCurrent()) return;
|
||||
commitConfirmedMemoryMode(changed.mode, modeTicket);
|
||||
@@ -890,6 +854,7 @@ export function OnboardingFlow({
|
||||
modeSaving,
|
||||
completionBusy,
|
||||
finishOnce,
|
||||
loadModePlan,
|
||||
queueModeWrite,
|
||||
queueProgressWrite,
|
||||
scheduleLoaded,
|
||||
@@ -947,7 +912,7 @@ export function OnboardingFlow({
|
||||
setModeError(null);
|
||||
const modeTicket = issueConfirmedModeTicket("onboarding");
|
||||
try {
|
||||
const plan = await planMemoryMode(target);
|
||||
const plan = await loadModePlan(target);
|
||||
if (!ownsConfirmation()) return;
|
||||
if (plan.blockers.length > 0) throw new Error(plan.blockers.join(","));
|
||||
const changed = await queueModeWrite(target, ownsConfirmation);
|
||||
@@ -994,6 +959,7 @@ export function OnboardingFlow({
|
||||
finishOnce,
|
||||
leave,
|
||||
leaveModelStep,
|
||||
loadModePlan,
|
||||
modeDestination,
|
||||
queueModeWrite,
|
||||
queueProgressWrite,
|
||||
@@ -1249,125 +1215,94 @@ export function OnboardingFlow({
|
||||
/>
|
||||
</div>
|
||||
{setupStep === "mode" && (
|
||||
<>
|
||||
<R6ColdStartFrame
|
||||
step={3}
|
||||
title={text("选择记忆模式")}
|
||||
focusToken={modeFocusToken}
|
||||
footer={
|
||||
<R6Button
|
||||
className="onboarding-r6-submit"
|
||||
variant="primary"
|
||||
pending={modeSaving || modePlanBusy}
|
||||
disabled={
|
||||
!modeChoice ||
|
||||
completionBusy ||
|
||||
modePlanBusy ||
|
||||
(modeChoice === "ultra" && !scheduleLoaded)
|
||||
}
|
||||
onClick={() => void saveModeChoice()}
|
||||
>
|
||||
{text(
|
||||
modePhase === "saving"
|
||||
? "正在保存模式…"
|
||||
: modePhase === "completing"
|
||||
? "正在完成首次设置…"
|
||||
: modeChoice === "lite"
|
||||
? "使用 Lite 进入"
|
||||
: modeChoice === "pro"
|
||||
? "继续配置模型"
|
||||
: "继续",
|
||||
)}
|
||||
</R6Button>
|
||||
}
|
||||
>
|
||||
<section
|
||||
className="onboarding-r6-mode"
|
||||
aria-label={text("记忆模式")}
|
||||
<R6ColdStartFrame
|
||||
step={3}
|
||||
title={text("选择记忆模式")}
|
||||
focusToken={modeFocusToken}
|
||||
footer={
|
||||
<R6Button
|
||||
className="onboarding-r6-submit"
|
||||
variant="primary"
|
||||
pending={modeSaving}
|
||||
disabled={
|
||||
!modeChoice ||
|
||||
completionBusy ||
|
||||
(modeChoice === "ultra" && !scheduleLoaded)
|
||||
}
|
||||
onClick={() => void saveModeChoice()}
|
||||
>
|
||||
<MemoryModeChoiceList
|
||||
aria-label={text("记忆模式")}
|
||||
modes={(["lite", "pro", "ultra"] as const).map((mode) => ({
|
||||
mode,
|
||||
title: text(MODE_COPY[mode].title),
|
||||
description: text(MODE_COPY[mode].description),
|
||||
}))}
|
||||
selected={modeChoice}
|
||||
disabled={modeSaving || completionBusy || modePlanBusy}
|
||||
onChange={(mode, control) =>
|
||||
void requestModeSelection(
|
||||
mode as OnboardingMemoryMode,
|
||||
control,
|
||||
)
|
||||
}
|
||||
/>
|
||||
{modeChoice === "ultra" && scheduleLoaded ? (
|
||||
<section
|
||||
className="r6-evolve-settings onboarding-r6-evolve-settings"
|
||||
aria-label={text("记忆整合(Evolve)")}
|
||||
>
|
||||
<UltraEvolveScheduleFields
|
||||
schedule={ultraSchedule}
|
||||
disabled={modeSaving || !scheduleLoaded}
|
||||
onEnabledChange={(enabled) =>
|
||||
setUltraSchedule((current) => ({
|
||||
...current,
|
||||
enabled,
|
||||
}))
|
||||
}
|
||||
onTimeChange={async (time) => {
|
||||
setUltraSchedule((current) => ({ ...current, time }));
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
) : null}
|
||||
{modeChoice === "ultra" && scheduleReadFailed ? (
|
||||
<div className="onboarding-r6-schedule-recovery">
|
||||
<p role="alert">
|
||||
{text("记忆模式状态暂时无法读取,请重试。")}
|
||||
</p>
|
||||
<R6Button
|
||||
className="onboarding-r6-schedule-retry"
|
||||
onClick={() => void refreshUltraSchedule()}
|
||||
>
|
||||
{text("重试")}
|
||||
</R6Button>
|
||||
</div>
|
||||
) : null}
|
||||
{(modeError || error) && (
|
||||
<p className="onboarding-r6-mode-error" role="alert">
|
||||
{text(modeError ?? error ?? "")}
|
||||
</p>
|
||||
{text(
|
||||
modePhase === "saving"
|
||||
? "正在保存模式…"
|
||||
: modePhase === "completing"
|
||||
? "正在完成首次设置…"
|
||||
: modeChoice === "lite"
|
||||
? "使用 Lite 进入"
|
||||
: modeChoice === "pro"
|
||||
? "继续配置模型"
|
||||
: "继续",
|
||||
)}
|
||||
</section>
|
||||
</R6ColdStartFrame>
|
||||
{pendingMode && pendingPlan ? (
|
||||
<MemoryModeConfirmDialog
|
||||
mode={pendingMode}
|
||||
plan={pendingPlan}
|
||||
busy={modePlanBusy}
|
||||
opener={modeDialogOpener.current}
|
||||
allowedBlockers={["active_model_required"]}
|
||||
onCancel={() => {
|
||||
modePlanOperation.current += 1;
|
||||
setModePlanBusy(false);
|
||||
setPendingMode(null);
|
||||
setPendingPlan(null);
|
||||
}}
|
||||
onConfirm={() => {
|
||||
modePlanOperation.current += 1;
|
||||
setModePlanBusy(false);
|
||||
</R6Button>
|
||||
}
|
||||
>
|
||||
<section
|
||||
className="onboarding-r6-mode"
|
||||
aria-label={text("记忆模式")}
|
||||
>
|
||||
<div className="onboarding-r6-mode-intro">
|
||||
<h2>
|
||||
{locale === "zh-Hans"
|
||||
? "Choose how MemHub remembers"
|
||||
: text("你希望 MemHub 如何形成记忆?")}
|
||||
</h2>
|
||||
<p>{text("需要明确选择一种方式;之后仍可在设置中更改。")}</p>
|
||||
</div>
|
||||
<MemoryModeCards
|
||||
aria-label={text("记忆模式")}
|
||||
value={modeChoice}
|
||||
disabled={modeSaving || completionBusy}
|
||||
onChange={(mode) => {
|
||||
modeChoiceTouched.current = true;
|
||||
intentRef.current = pendingMode;
|
||||
setModeChoice(pendingMode);
|
||||
intentRef.current = mode;
|
||||
setModeChoice(mode);
|
||||
setModeError(null);
|
||||
setPendingMode(null);
|
||||
setPendingPlan(null);
|
||||
}}
|
||||
ultraDetail={
|
||||
scheduleLoaded ? (
|
||||
<div className="onboarding-r6-ultra-default">
|
||||
<strong>{text("每天自动整合")}</strong>
|
||||
<small>
|
||||
{text(
|
||||
ultraSchedule.enabled
|
||||
? "每天 {time} 左右自动整合,使用这台设备的本地时间;休眠错过后会补跑。"
|
||||
: "已关闭。重新开启后仍按 {time} 运行;已生成的结果会保留。",
|
||||
).replace("{time}", ultraSchedule.time)}
|
||||
</small>
|
||||
</div>
|
||||
) : scheduleReadFailed ? (
|
||||
<div className="onboarding-r6-schedule-recovery">
|
||||
<p role="alert">
|
||||
{text("记忆模式状态暂时无法读取,请重试。")}
|
||||
</p>
|
||||
<R6Button
|
||||
className="onboarding-r6-schedule-retry"
|
||||
onClick={() => void refreshUltraSchedule()}
|
||||
>
|
||||
{text("重试")}
|
||||
</R6Button>
|
||||
</div>
|
||||
) : (
|
||||
<p>{text("正在读取自动整合设置…")}</p>
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
{(modeError || error) && (
|
||||
<p className="onboarding-r6-mode-error" role="alert">
|
||||
{text(modeError ?? error ?? "")}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</R6ColdStartFrame>
|
||||
)}
|
||||
{visitedModel && (
|
||||
<div hidden={setupStep !== "model"}>
|
||||
|
||||
@@ -9,7 +9,7 @@ function declaration(file: string, selector: string): string {
|
||||
|
||||
it("fills settings controls without widening shared fallback controls", () => {
|
||||
for (const selector of [
|
||||
".r6-memory-mode-settings .memory-mode-choice-list",
|
||||
".r6-memory-mode-settings .memory-mode-cards",
|
||||
".r6-settings .connection-fallback .custom-harness-name",
|
||||
]) {
|
||||
const rule = declaration("r6-settings.css", selector);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
/* 143:2419/2502/2594/2685: only step2; shared frame/progress are owned elsewhere. */
|
||||
.first-import-r6-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 738px) 278px;
|
||||
gap: 30px;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
min-height: 100%;
|
||||
height: auto;
|
||||
}
|
||||
.first-import-r6-main {
|
||||
min-width: 0;
|
||||
@@ -13,6 +12,22 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.first-import-r6-intro {
|
||||
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: #63707b;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.first-import-r6-history-window {
|
||||
margin: 0 0 12px;
|
||||
padding: 8px 10px;
|
||||
@@ -25,6 +40,101 @@
|
||||
}
|
||||
.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"] {
|
||||
width: min(680px, 100%);
|
||||
grid-auto-columns: 100%;
|
||||
}
|
||||
.first-import-r6-main > .r6-import-progress[data-count="2"] {
|
||||
width: min(720px, 100%);
|
||||
grid-auto-columns: calc((100% - 16px) / 2);
|
||||
}
|
||||
.first-import-r6-main > .r6-import-progress .r6-import-progress-card {
|
||||
min-height: 112px;
|
||||
padding: 12px 18px;
|
||||
display: grid;
|
||||
grid-template-columns: 82px minmax(0, 1fr);
|
||||
grid-template-rows: auto auto auto;
|
||||
align-content: center;
|
||||
column-gap: 16px;
|
||||
text-align: left;
|
||||
}
|
||||
.first-import-r6-main > .r6-import-progress .r6-import-progress-orbit {
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 4;
|
||||
margin: 0;
|
||||
}
|
||||
.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-main > .r6-import-progress[data-count="many"] {
|
||||
grid-auto-flow: row;
|
||||
grid-auto-columns: unset;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:not(:has(> :nth-child(4))) {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:has(> :nth-child(4)):not(
|
||||
:has(> :nth-child(5))
|
||||
) {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:has(> :nth-child(5)):not(
|
||||
:has(> :nth-child(6))
|
||||
) {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:has(> :nth-child(6)):not(
|
||||
:has(> :nth-child(7))
|
||||
) {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:has(> :nth-child(7)) {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:has(> :nth-child(4))
|
||||
.r6-import-progress-card {
|
||||
min-height: 112px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
justify-items: center;
|
||||
gap: 1px;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:has(> :nth-child(4))
|
||||
.r6-import-progress-orbit {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
grid-column: 1;
|
||||
grid-row: auto;
|
||||
}
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:has(> :nth-child(4))
|
||||
:is(h2, p, small) {
|
||||
grid-column: 1;
|
||||
align-self: auto;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.first-import-r6-main
|
||||
> .r6-import-progress[data-count="many"]:has(> :nth-child(4))
|
||||
.r6-import-progress-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.first-import-r6-feed {
|
||||
flex: 0 0 auto;
|
||||
@@ -76,9 +186,14 @@
|
||||
margin: 0;
|
||||
}
|
||||
.first-import-r6-options {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 24px;
|
||||
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;
|
||||
border: 1px solid #ffffffe0;
|
||||
border-radius: 22px;
|
||||
background:
|
||||
@@ -87,11 +202,14 @@
|
||||
box-shadow:
|
||||
inset 0 1px #fff,
|
||||
0 12px 32px rgb(49 75 101 / 4%);
|
||||
overflow: auto;
|
||||
overflow: visible;
|
||||
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;
|
||||
font-size: 12px;
|
||||
@@ -117,12 +235,12 @@
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 124px;
|
||||
margin: 0 0 16px;
|
||||
padding: 18px 16px 12px;
|
||||
min-height: 104px;
|
||||
margin: 0;
|
||||
padding: 18px 52px 16px 18px;
|
||||
border: 1px solid rgb(179 196 209 / 42%);
|
||||
border-radius: 0;
|
||||
background: rgb(255 255 255 / 10%);
|
||||
border-radius: 12px;
|
||||
background: rgb(255 255 255 / 34%);
|
||||
box-shadow: inset 0 1px 1px #ffffffd9;
|
||||
backdrop-filter: blur(20px);
|
||||
cursor: pointer;
|
||||
@@ -151,8 +269,8 @@
|
||||
.first-import-r6-selection input {
|
||||
appearance: none;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
right: 18px;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
min-height: 16px;
|
||||
@@ -190,10 +308,11 @@
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
margin-top: 8px;
|
||||
margin-top: 7px;
|
||||
color: #5e6e7a;
|
||||
}
|
||||
.first-import-r6-options-note {
|
||||
grid-column: 1 / -1;
|
||||
font-size: 11px;
|
||||
line-height: 22px;
|
||||
margin: 0 0 12px;
|
||||
@@ -390,8 +509,7 @@ dialog.first-import-r6-original[open] {
|
||||
}
|
||||
@media (max-width: 1150px) {
|
||||
.first-import-r6-layout {
|
||||
grid-template-columns: minmax(0, 1fr) 278px;
|
||||
gap: 24px;
|
||||
gap: 18px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 761px) and (max-height: 959px) {
|
||||
@@ -401,12 +519,12 @@ dialog.first-import-r6-original[open] {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.first-import-r6-layout {
|
||||
height: auto;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@media (min-width: 761px) and (max-height: 859px) {
|
||||
.first-import-r6-options {
|
||||
padding: 14px;
|
||||
padding: 18px 20px;
|
||||
overflow: visible;
|
||||
}
|
||||
.first-import-r6-eyebrow {
|
||||
@@ -424,16 +542,16 @@ dialog.first-import-r6-original[open] {
|
||||
}
|
||||
.first-import-r6-selection,
|
||||
.first-import-r6-selection + .first-import-r6-selection {
|
||||
min-height: 86px;
|
||||
margin-bottom: 8px;
|
||||
padding: 9px 12px;
|
||||
min-height: 96px;
|
||||
margin-bottom: 0;
|
||||
padding: 15px 48px 13px 16px;
|
||||
}
|
||||
.first-import-r6-selection strong {
|
||||
line-height: 24px;
|
||||
}
|
||||
.first-import-r6-selection input {
|
||||
top: 13px;
|
||||
right: 12px;
|
||||
top: 17px;
|
||||
right: 16px;
|
||||
}
|
||||
.first-import-r6-selection span {
|
||||
margin-top: 4px;
|
||||
@@ -454,6 +572,7 @@ dialog.first-import-r6-original[open] {
|
||||
overflow: visible;
|
||||
}
|
||||
.first-import-r6-options {
|
||||
display: block;
|
||||
overflow: visible;
|
||||
}
|
||||
.first-import-r6-footer-note {
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
.memory-mode-cards {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.memory-mode-card {
|
||||
min-width: 0;
|
||||
min-height: 286px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border: 1px solid #c7d3dc;
|
||||
border-radius: 15px;
|
||||
background: rgb(255 255 255 / 66%);
|
||||
color: #304757;
|
||||
box-shadow: inset 0 1px rgb(255 255 255 / 82%);
|
||||
transition:
|
||||
border-color 140ms ease-out,
|
||||
background-color 140ms ease-out,
|
||||
box-shadow 140ms ease-out,
|
||||
transform 140ms ease-out;
|
||||
}
|
||||
|
||||
.memory-mode-card:has(.memory-mode-card-choice:hover):not(
|
||||
[data-disabled="true"]
|
||||
) {
|
||||
border-color: #91a7b6;
|
||||
background: rgb(255 255 255 / 82%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.memory-mode-card[data-selected="true"] {
|
||||
border: 2px solid #6f9183;
|
||||
background: #fbfdfc;
|
||||
box-shadow: 0 10px 26px rgb(47 107 87 / 8%);
|
||||
}
|
||||
|
||||
.memory-mode-card[data-disabled="true"] {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.memory-mode-card-choice {
|
||||
min-height: 284px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.memory-mode-card[data-disabled="true"] .memory-mode-card-choice {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.memory-mode-card-choice > input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.memory-mode-card:has(input:focus-visible) {
|
||||
outline: 2px solid var(--r6-focus, #557c93);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.memory-mode-title {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.memory-mode-index {
|
||||
flex: 0 0 auto;
|
||||
color: var(--ok, #2f6b57);
|
||||
font-size: 11px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.memory-mode-title strong {
|
||||
min-width: 0;
|
||||
color: #263947;
|
||||
font-size: 19px;
|
||||
font-weight: 600;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.memory-mode-tier {
|
||||
flex: 0 0 auto;
|
||||
color: var(--ok, #2f6b57);
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.memory-mode-current {
|
||||
flex: 0 0 auto;
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
color: #526a60;
|
||||
background: #edf4f0;
|
||||
font-size: 10px;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
.memory-mode-radio {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
margin-left: auto;
|
||||
flex: 0 0 21px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid #8da0ae;
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
background: rgb(255 255 255 / 82%);
|
||||
}
|
||||
|
||||
.memory-mode-card[data-selected="true"] .memory-mode-radio {
|
||||
border-color: var(--ok, #2f6b57);
|
||||
background: var(--ok, #2f6b57);
|
||||
}
|
||||
|
||||
.memory-mode-radio svg {
|
||||
stroke: #fff;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.memory-mode-description {
|
||||
min-height: 50px;
|
||||
margin-top: 10px;
|
||||
color: #526674;
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.memory-mode-features {
|
||||
margin-top: auto;
|
||||
padding: 16px 0 2px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
border-top: 1px solid #dbe4ea;
|
||||
color: #5e6e7a;
|
||||
font-size: 12px;
|
||||
line-height: 19px;
|
||||
}
|
||||
|
||||
.memory-mode-features > span {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.memory-mode-features i {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin-top: 7px;
|
||||
flex: 0 0 5px;
|
||||
border-radius: 50%;
|
||||
background: #7f95a4;
|
||||
}
|
||||
|
||||
.memory-mode-detail {
|
||||
margin-top: auto;
|
||||
padding: 14px 18px;
|
||||
border-top: 1px solid #d4e0e7;
|
||||
background: #f1f6f8;
|
||||
color: #405867;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
animation: memory-mode-detail-in 180ms ease-out both;
|
||||
}
|
||||
|
||||
@keyframes memory-mode-detail-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 681px) and (max-height: 959px) {
|
||||
.memory-mode-cards {
|
||||
gap: 14px;
|
||||
}
|
||||
.memory-mode-card {
|
||||
min-height: 194px;
|
||||
}
|
||||
.memory-mode-card-choice {
|
||||
min-height: 194px;
|
||||
padding: 16px;
|
||||
}
|
||||
.memory-mode-title strong {
|
||||
font-size: 17px;
|
||||
line-height: 25px;
|
||||
}
|
||||
.memory-mode-description {
|
||||
min-height: 36px;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.memory-mode-features {
|
||||
padding-top: 10px;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.memory-mode-detail {
|
||||
padding: 10px 14px;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.memory-mode-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.memory-mode-card,
|
||||
.memory-mode-card-choice {
|
||||
min-height: 0;
|
||||
}
|
||||
.memory-mode-description {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.memory-mode-card,
|
||||
.memory-mode-detail {
|
||||
animation: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -594,7 +594,7 @@
|
||||
}
|
||||
.onboarding-model-layout {
|
||||
display: block;
|
||||
width: min(760px, 100%);
|
||||
width: min(880px, 100%);
|
||||
height: auto;
|
||||
margin-inline: auto;
|
||||
}
|
||||
@@ -603,11 +603,13 @@
|
||||
}
|
||||
.onboarding-model-group {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
padding: 24px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18px 20px;
|
||||
padding: 24px 26px;
|
||||
border: 1px solid #d8e2e8;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
background: rgb(255 255 255 / 72%);
|
||||
box-shadow: inset 0 1px rgb(255 255 255 / 88%);
|
||||
}
|
||||
.onboarding-model-provider,
|
||||
.onboarding-model-key {
|
||||
@@ -639,6 +641,7 @@
|
||||
line-height: 19px;
|
||||
}
|
||||
.onboarding-model-default-summary {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 14px 0;
|
||||
@@ -656,10 +659,12 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
.onboarding-model-disclosure {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
.generation-technical-disclosure {
|
||||
grid-column: 1 / -1;
|
||||
border-top: 1px solid #e4eaee;
|
||||
padding-top: 4px;
|
||||
}
|
||||
@@ -750,8 +755,27 @@
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
@media (min-width: 761px) and (max-height: 959px) {
|
||||
.onboarding-model-r6 .onboarding-r6-body {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.onboarding-model-layout,
|
||||
.onboarding-model-form {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
.onboarding-model-form {
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.onboarding-model-group {
|
||||
gap: 14px 18px;
|
||||
padding: 18px 22px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.onboarding-model-layout--single-column-600 .onboarding-model-group {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@@ -225,6 +225,22 @@
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.onboarding-r6-body:has(> .onboarding-r6-mode) {
|
||||
padding-block: 14px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.onboarding-r6-mode {
|
||||
width: 100%;
|
||||
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-discovery {
|
||||
height: 100%;
|
||||
@@ -541,41 +557,36 @@
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
.onboarding-r6-mode-intro {
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
}
|
||||
.onboarding-r6-mode-intro h2 {
|
||||
margin: 0;
|
||||
color: #263947;
|
||||
font-size: 22px;
|
||||
line-height: 32px;
|
||||
}
|
||||
.onboarding-r6-mode-intro p {
|
||||
margin: 0;
|
||||
margin: 4px 0 0;
|
||||
color: #637381;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
.onboarding-r6-mode .memory-mode-choice-list {
|
||||
background: rgb(255 255 255 / 72%);
|
||||
.onboarding-r6-ultra-default {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
.onboarding-r6-mode .memory-mode-choice {
|
||||
min-height: 82px;
|
||||
padding: 15px 18px;
|
||||
}
|
||||
.onboarding-r6-mode .memory-mode-choice-copy {
|
||||
gap: 4px;
|
||||
}
|
||||
.onboarding-r6-mode .memory-mode-choice-copy strong {
|
||||
font-size: 16px;
|
||||
line-height: 25px;
|
||||
}
|
||||
.onboarding-r6-mode .memory-mode-choice-copy small {
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
}
|
||||
.onboarding-r6-mode-ultra-note {
|
||||
margin: -6px 0 0;
|
||||
color: #637381;
|
||||
.onboarding-r6-ultra-default strong {
|
||||
color: #304757;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
.onboarding-r6-ultra-default small {
|
||||
color: #607482;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.onboarding-r6-evolve-settings {
|
||||
margin-top: 0;
|
||||
|
||||
@@ -227,10 +227,27 @@
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
}
|
||||
.r6-memory-mode-settings .memory-mode-choice-list {
|
||||
.r6-memory-mode-settings .memory-mode-cards {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
.r6-memory-mode-settings
|
||||
.memory-mode-card[data-mode="ultra"][data-selected="true"] {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.r6-memory-mode-settings
|
||||
.memory-mode-card[data-mode="ultra"][data-selected="true"]
|
||||
.memory-mode-card-choice {
|
||||
min-height: 180px;
|
||||
}
|
||||
.r6-evolve-settings--in-card {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
.r6-evolve-settings--in-card .r6-evolve-setting:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.r6-settings-note {
|
||||
margin: 14px 0 0;
|
||||
padding: 10px 12px;
|
||||
|
||||
Reference in new issue
Block a user