Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38ea7bb041 | ||
|
|
865c66de52 |
No files matched your search
@@ -72,15 +72,23 @@ it("localizes the whole home live without repeating reads or changing imported t
|
||||
expect(summary).toHaveBeenCalledTimes(calls);
|
||||
});
|
||||
|
||||
const { summary, projection, processing, sedimentation, history, liveSync } =
|
||||
vi.hoisted(() => ({
|
||||
summary: vi.fn(),
|
||||
projection: vi.fn(),
|
||||
processing: vi.fn(),
|
||||
sedimentation: vi.fn(),
|
||||
history: vi.fn(),
|
||||
liveSync: vi.fn(),
|
||||
}));
|
||||
const {
|
||||
summary,
|
||||
projection,
|
||||
processing,
|
||||
sedimentation,
|
||||
history,
|
||||
liveSync,
|
||||
evolve,
|
||||
} = vi.hoisted(() => ({
|
||||
summary: vi.fn(),
|
||||
projection: vi.fn(),
|
||||
processing: vi.fn(),
|
||||
sedimentation: vi.fn(),
|
||||
history: vi.fn(),
|
||||
liveSync: vi.fn(),
|
||||
evolve: vi.fn(),
|
||||
}));
|
||||
vi.mock("./call-history-client", async (original) => ({
|
||||
...(await original<typeof import("./call-history-client")>()),
|
||||
getCallHistory: history,
|
||||
@@ -105,6 +113,7 @@ vi.mock("./api", () => ({
|
||||
getSdkRaw: vi.fn(),
|
||||
getLiveSyncHealth: liveSync,
|
||||
getMemoryProcessing: processing,
|
||||
getEvolveOverview: evolve,
|
||||
getSedimentation: sedimentation,
|
||||
getSources: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
@@ -146,6 +155,12 @@ beforeEach(() => {
|
||||
history
|
||||
.mockReset()
|
||||
.mockResolvedValue({ available: true, recording_error: false, items: [] });
|
||||
evolve.mockReset().mockResolvedValue({
|
||||
mode: "ultra",
|
||||
schedule: { enabled: true, time: "03:00" },
|
||||
users: {},
|
||||
running: [],
|
||||
});
|
||||
imports.refresh = vi.fn();
|
||||
summary.mockReset().mockResolvedValue({
|
||||
memory_count: 601,
|
||||
@@ -940,6 +955,39 @@ it.each([
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps the home snapshot visible during Evolve and refreshes when it ends", async () => {
|
||||
evolve.mockResolvedValue({
|
||||
mode: "ultra",
|
||||
schedule: { enabled: true, time: "03:00" },
|
||||
users: {},
|
||||
running: [
|
||||
{
|
||||
run_id: "run-1",
|
||||
user_id: "local",
|
||||
state: "running",
|
||||
},
|
||||
],
|
||||
});
|
||||
render(mountHome());
|
||||
|
||||
expect(await screen.findByText("601")).toBeTruthy();
|
||||
expect(await screen.findByText("正在整合记忆…")).toBeTruthy();
|
||||
const readsBeforeCompletion = projection.mock.calls.length;
|
||||
|
||||
act(() => {
|
||||
queryClient.setQueryData(["evolve", "local"], {
|
||||
mode: "ultra",
|
||||
schedule: { enabled: true, time: "03:00" },
|
||||
users: {},
|
||||
running: [],
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(projection.mock.calls.length).toBeGreaterThan(readsBeforeCompletion),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
resetClient();
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
retryAutoReconcile,
|
||||
useAutoConnect,
|
||||
} from "./connection-reconcile";
|
||||
import { localEvolveRunning, useEvolveState } from "./evolve-state";
|
||||
import { useGuiLanguage } from "./gui-language";
|
||||
import { HomeCardBoundary } from "./HomeCardBoundary";
|
||||
import {
|
||||
@@ -55,6 +56,7 @@ import {
|
||||
sourceConnectionIncomplete,
|
||||
useRecallHosts,
|
||||
} from "./host-state";
|
||||
import { useOnboardingTranslation } from "./onboarding-language";
|
||||
import { subscribePoll } from "./poll-scheduler";
|
||||
import { R6Button } from "./R6Controls";
|
||||
import { prefetchDataRuntime } from "./runtime-state";
|
||||
@@ -209,6 +211,10 @@ export function ApplicationsHome({
|
||||
active?: boolean;
|
||||
}) {
|
||||
const { locale, t, tn } = useGuiLanguage();
|
||||
const { text } = useOnboardingTranslation();
|
||||
const evolveState = useEvolveState(active);
|
||||
const evolveRunning = localEvolveRunning(evolveState.data);
|
||||
const evolveUncertain = evolveState.isError && evolveRunning;
|
||||
const hostQuery = useRecallHosts({ poll: active });
|
||||
const hostBridge = hostBridgeAvailable();
|
||||
const hosts = hostQuery.data ?? [];
|
||||
@@ -313,6 +319,12 @@ export function ApplicationsHome({
|
||||
const [expanded, setExpanded] = useState(retained.expanded);
|
||||
const [phase, setPhase] = useState("idle");
|
||||
const [revision, setRevision] = useState(0);
|
||||
const previousEvolveRunning = useRef(evolveRunning);
|
||||
useEffect(() => {
|
||||
const previous = previousEvolveRunning.current;
|
||||
previousEvolveRunning.current = evolveRunning;
|
||||
if (previous && !evolveRunning && active) setRevision((value) => value + 1);
|
||||
}, [active, evolveRunning]);
|
||||
const confirmedModeSeen = useRef(confirmedMode);
|
||||
const expandButton = useRef<HTMLButtonElement>(null);
|
||||
const backButton = useRef<HTMLButtonElement>(null);
|
||||
@@ -953,6 +965,11 @@ export function ApplicationsHome({
|
||||
<header>
|
||||
<h2>{t("apps.overview")}</h2>
|
||||
</header>
|
||||
{evolveRunning ? (
|
||||
<p className="r6-evolve-status" role="status">
|
||||
{text(evolveUncertain ? "正在核对整合状态…" : "正在整合记忆…")}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="r6-home-metrics">
|
||||
<div role="status" aria-live="polite" aria-atomic="true">
|
||||
<p>{t(viewModel.countLabel)}</p>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
setMemoryMode,
|
||||
} from "./api";
|
||||
import { MemoryModeSettings } from "./MemoryModeSettings";
|
||||
import { resetClient } from "./query-client";
|
||||
|
||||
vi.mock("./api", () => ({
|
||||
getMemoryMode: vi.fn(),
|
||||
@@ -81,6 +82,7 @@ async function chooseMode(
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
resetClient();
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(apiErrorCode).mockReturnValue(null);
|
||||
vi.mocked(memoryModePlanUnsupported).mockReturnValue(false);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type {
|
||||
EvolveOverview,
|
||||
EvolveRunReceipt,
|
||||
EvolveSchedule,
|
||||
MemoryModeChange,
|
||||
MemoryModePlan,
|
||||
@@ -11,7 +9,6 @@ import { legacyMemoryModePlan } from "@one-memory/operations";
|
||||
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
||||
import {
|
||||
apiErrorCode,
|
||||
getEvolveOverview,
|
||||
getEvolveSchedule,
|
||||
getMemoryMode,
|
||||
getMemoryProcessing,
|
||||
@@ -25,6 +22,7 @@ import {
|
||||
commitConfirmedMemoryMode,
|
||||
issueConfirmedModeTicket,
|
||||
} from "./confirmed-memory-mode";
|
||||
import { commitEvolveRun, useEvolveState } from "./evolve-state";
|
||||
import { MemoryModeCards } from "./MemoryModeCards";
|
||||
import { BLOCKER_COPY, MemoryModeConfirmDialog } from "./memory-mode-ui";
|
||||
import { useOnboardingTranslation } from "./onboarding-language";
|
||||
@@ -47,7 +45,11 @@ export function MemoryModeSettings({
|
||||
const id = useId();
|
||||
const [state, setState] = useState<MemoryModeState | null>(null);
|
||||
const [schedule, setScheduleState] = useState<EvolveSchedule | null>(null);
|
||||
const [overview, setOverview] = useState<EvolveOverview | null>(null);
|
||||
const evolveState = useEvolveState();
|
||||
const overview = evolveState.data ?? null;
|
||||
const run =
|
||||
overview?.running.find((candidate) => candidate.user_id === "local") ??
|
||||
null;
|
||||
const [pendingMode, setPendingMode] = useState<MemoryPipelineMode | null>(
|
||||
null,
|
||||
);
|
||||
@@ -56,7 +58,6 @@ export function MemoryModeSettings({
|
||||
const [maintenance, setMaintenance] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [readFailed, setReadFailed] = useState(false);
|
||||
const [run, setRun] = useState<EvolveRunReceipt | null>(null);
|
||||
const refreshVersion = useRef(0);
|
||||
const dialogOpener = useRef<HTMLElement | null>(null);
|
||||
const focusEvolveWhenReady = useRef(false);
|
||||
@@ -83,11 +84,10 @@ export function MemoryModeSettings({
|
||||
const refresh = useCallback(async () => {
|
||||
const version = ++refreshVersion.current;
|
||||
const modeTicket = issueConfirmedModeTicket("settings");
|
||||
const [modeResult, scheduleResult, overviewResult, processingResult] =
|
||||
const [modeResult, scheduleResult, processingResult] =
|
||||
await Promise.allSettled([
|
||||
getMemoryMode(),
|
||||
getEvolveSchedule(),
|
||||
getEvolveOverview(),
|
||||
getMemoryProcessing(),
|
||||
]);
|
||||
if (version !== refreshVersion.current) return;
|
||||
@@ -103,10 +103,6 @@ export function MemoryModeSettings({
|
||||
}
|
||||
if (scheduleResult.status === "fulfilled")
|
||||
setScheduleState(scheduleResult.value);
|
||||
if (overviewResult.status === "fulfilled") {
|
||||
setOverview(overviewResult.value);
|
||||
setRun(overviewResult.value.running[0] ?? null);
|
||||
}
|
||||
if (processingResult.status === "fulfilled")
|
||||
setMaintenance(Boolean(processingResult.value.embedding_switch));
|
||||
}, [onModeChange]);
|
||||
@@ -116,14 +112,11 @@ export function MemoryModeSettings({
|
||||
}, [refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
const running =
|
||||
maintenance ||
|
||||
state?.graph_build.state === "running" ||
|
||||
run?.state === "running";
|
||||
const running = maintenance || state?.graph_build.state === "running";
|
||||
if (!running) return;
|
||||
const timer = window.setInterval(() => void refresh(), 1500);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [maintenance, refresh, run?.state, state?.graph_build.state]);
|
||||
}, [maintenance, refresh, state?.graph_build.state]);
|
||||
|
||||
const lifecycleBusy =
|
||||
maintenance ||
|
||||
@@ -202,13 +195,21 @@ export function MemoryModeSettings({
|
||||
}
|
||||
|
||||
async function evolveNow() {
|
||||
if (!state || !schedule) return;
|
||||
refreshVersion.current++;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
const receipt = await startEvolve();
|
||||
setRun(receipt);
|
||||
await refresh();
|
||||
commitEvolveRun(
|
||||
receipt,
|
||||
overview ?? {
|
||||
mode: state.mode,
|
||||
schedule,
|
||||
users: {},
|
||||
running: [],
|
||||
},
|
||||
);
|
||||
} catch (reason) {
|
||||
const code = apiErrorCode(reason);
|
||||
setError(
|
||||
|
||||
@@ -12,6 +12,7 @@ import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { LANGUAGE_STORAGE_KEY, LanguageProvider } from "./gui-language";
|
||||
import { MemorySpace } from "./MemorySpace";
|
||||
import { queryClient, resetClient } from "./query-client";
|
||||
|
||||
it("translates memory filters and detail affordances live without refetching", async () => {
|
||||
setup();
|
||||
@@ -79,6 +80,7 @@ const { reads } = vi.hoisted(() => ({
|
||||
raw: vi.fn(),
|
||||
rawGraph: vi.fn(),
|
||||
rawSearch: vi.fn(),
|
||||
evolve: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock("./api", async (importOriginal) => ({
|
||||
@@ -90,6 +92,7 @@ vi.mock("./api", async (importOriginal) => ({
|
||||
getSdkRaw: reads.raw,
|
||||
getSdkRawGraph: reads.rawGraph,
|
||||
searchSdkRaw: reads.rawSearch,
|
||||
getEvolveOverview: reads.evolve,
|
||||
}));
|
||||
vi.mock("./GraphWidthPrompt", () => ({ GraphWidthPrompt: () => null }));
|
||||
vi.mock("./MemoryAddComposer", () => ({
|
||||
@@ -119,13 +122,19 @@ vi.mock("./MemoryAddComposer", () => ({
|
||||
),
|
||||
}));
|
||||
vi.mock("./MemoryGraph3D", () => ({
|
||||
MemoryGraph3D: () => <div data-testid="graph-owner" />,
|
||||
MemoryGraph3D: ({ graph }: { graph?: { nodes?: unknown[] } | null }) => (
|
||||
<div
|
||||
data-testid="graph-owner"
|
||||
data-graph-nodes={graph?.nodes?.length ?? 0}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
vi.mock("./RawConversationGraph3D", () => ({
|
||||
RawConversationGraph3D: () => <div data-testid="graph-owner" />,
|
||||
}));
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
resetClient();
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
@@ -159,7 +168,94 @@ it("translates the model-required prompt without configuring a model", async ()
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("keeps the memory snapshot visible during Evolve and refreshes when it ends", async () => {
|
||||
setup();
|
||||
reads.processing.mockResolvedValue(modeProcessing("ultra"));
|
||||
reads.evolve.mockResolvedValue({
|
||||
mode: "ultra",
|
||||
schedule: { enabled: true, time: "03:00" },
|
||||
users: {},
|
||||
running: [
|
||||
{
|
||||
run_id: "run-1",
|
||||
user_id: "local",
|
||||
state: "running",
|
||||
},
|
||||
],
|
||||
});
|
||||
render(<MemorySpace simplified />);
|
||||
|
||||
expect((await screen.findAllByText("已保存的记忆")).length).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
expect(await screen.findByText("正在整合记忆…")).toBeTruthy();
|
||||
const readsBeforeCompletion = reads.sdkLibrary.mock.calls.length;
|
||||
|
||||
act(() => {
|
||||
queryClient.setQueryData(["evolve", "local"], {
|
||||
mode: "ultra",
|
||||
schedule: { enabled: true, time: "03:00" },
|
||||
users: {},
|
||||
running: [],
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(reads.sdkLibrary.mock.calls.length).toBeGreaterThan(
|
||||
readsBeforeCompletion,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the last successful graph when its Evolve refresh fails", async () => {
|
||||
setup();
|
||||
reads.processing.mockResolvedValue(modeProcessing("ultra"));
|
||||
reads.evolve.mockResolvedValue({
|
||||
mode: "ultra",
|
||||
schedule: { enabled: true, time: "03:00" },
|
||||
users: {},
|
||||
running: [{ run_id: "run-1", user_id: "local", state: "running" }],
|
||||
});
|
||||
const firstSource = {
|
||||
source_id: "cursor",
|
||||
display_name: "Cursor",
|
||||
authorized: true,
|
||||
sync_enabled: true,
|
||||
imported_messages: 1,
|
||||
imported_conversations: 1,
|
||||
} as SourceResponse;
|
||||
const view = render(<MemorySpace simplified sources={[firstSource]} />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("graph-owner").dataset.graphNodes).toBe("1"),
|
||||
);
|
||||
reads.sdkGraph.mockRejectedValueOnce(new Error("evolve write lock"));
|
||||
|
||||
view.rerender(
|
||||
<MemorySpace
|
||||
simplified
|
||||
sources={[
|
||||
firstSource,
|
||||
{
|
||||
...firstSource,
|
||||
source_id: "codex",
|
||||
display_name: "Codex",
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(reads.sdkGraph).toHaveBeenCalledTimes(2));
|
||||
expect(screen.getByTestId("graph-owner").dataset.graphNodes).toBe("1");
|
||||
});
|
||||
|
||||
function setup() {
|
||||
reads.evolve.mockResolvedValue({
|
||||
mode: "ultra",
|
||||
schedule: { enabled: true, time: "03:00" },
|
||||
users: {},
|
||||
running: [],
|
||||
});
|
||||
const memories = [
|
||||
{
|
||||
memory_id: "m1",
|
||||
|
||||
@@ -45,10 +45,12 @@ import {
|
||||
setExtractionWindow,
|
||||
updateSdkMemory,
|
||||
} from "./api";
|
||||
import { localEvolveRunning, useEvolveState } from "./evolve-state";
|
||||
import { GraphWidthPrompt } from "./GraphWidthPrompt";
|
||||
import { useGuiLanguage } from "./gui-language";
|
||||
import { MemoryDeleteDialog } from "./MemoryDeleteDialog";
|
||||
import { MemoryMap } from "./MemoryMap";
|
||||
import { useOnboardingTranslation } from "./onboarding-language";
|
||||
import { subscribePoll } from "./poll-scheduler";
|
||||
import { SdkMemoryLibrary } from "./SdkMemoryLibrary";
|
||||
import { SdkRawHistory } from "./SdkRawHistory";
|
||||
@@ -119,6 +121,10 @@ export function MemorySpace({
|
||||
onFocusMemoryConsumed?(): void;
|
||||
}) {
|
||||
const { locale, t, tn } = useGuiLanguage();
|
||||
const { text } = useOnboardingTranslation();
|
||||
const evolveState = useEvolveState(active || preload);
|
||||
const evolveRunning = localEvolveRunning(evolveState.data);
|
||||
const evolveUncertain = evolveState.isError && evolveRunning;
|
||||
const memoryText = (text: string) => {
|
||||
const key = `memory:${text}`;
|
||||
return isMessageKey(key) ? t(key) : text;
|
||||
@@ -203,6 +209,12 @@ export function MemorySpace({
|
||||
// Bumping this re-reads the page and graph after an edit or a delete.
|
||||
const [sdkMapRevision, setSdkMapRevision] = useState(0);
|
||||
const bumpSdkMap = useCallback(() => setSdkMapRevision((v) => v + 1), []);
|
||||
const previousEvolveRunning = useRef(evolveRunning);
|
||||
useEffect(() => {
|
||||
const previous = previousEvolveRunning.current;
|
||||
previousEvolveRunning.current = evolveRunning;
|
||||
if (previous && !evolveRunning) bumpSdkMap();
|
||||
}, [bumpSdkMap, evolveRunning]);
|
||||
const [sdkLibrary, setSdkLibrary] = useState<{
|
||||
page: MemoryLibraryPage | null;
|
||||
loading: boolean;
|
||||
@@ -257,7 +269,8 @@ export function MemorySpace({
|
||||
]);
|
||||
const sdkGraphSettled = sdkGraph?.scope === sdkGraphRequestedScope;
|
||||
const visibleSdkGraph =
|
||||
sdkGraph?.scope === sdkGraphRequestedScope && sdkGraph.status === "ready"
|
||||
sdkGraph?.scope === sdkGraphRequestedScope &&
|
||||
(sdkGraph.status === "ready" || evolveRunning)
|
||||
? sdkGraph.value
|
||||
: null;
|
||||
const sdkGraphFailed =
|
||||
@@ -510,7 +523,11 @@ export function MemorySpace({
|
||||
}
|
||||
} catch {
|
||||
if (cancelled || request.signal.aborted) return;
|
||||
setSdkGraph({ scope, status: "failed", value: null });
|
||||
setSdkGraph((current) => ({
|
||||
scope,
|
||||
status: "failed",
|
||||
value: current?.scope === scope ? current.value : null,
|
||||
}));
|
||||
} finally {
|
||||
reading = false;
|
||||
}
|
||||
@@ -1187,6 +1204,11 @@ export function MemorySpace({
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
{evolveRunning ? (
|
||||
<p className="memory-evolve-status" role="status">
|
||||
{text(evolveUncertain ? "正在核对整合状态…" : "正在整合记忆…")}
|
||||
</p>
|
||||
) : null}
|
||||
{processing && (!simplified || managementOpen) ? (
|
||||
<details className="memory-processing-details">
|
||||
<summary>
|
||||
@@ -1372,9 +1394,9 @@ export function MemorySpace({
|
||||
source: sdkLibrarySource,
|
||||
page: displayedSdkPage,
|
||||
loading: sdkLibrary.loading,
|
||||
failed: sdkLibrary.failed,
|
||||
failed: sdkLibrary.failed && !evolveRunning,
|
||||
graphLoading: !sdkGraphSettled,
|
||||
graphFailed: sdkGraphFailed,
|
||||
graphFailed: sdkGraphFailed && !evolveRunning,
|
||||
graphLoaded: visibleGraphProgress?.loaded,
|
||||
graphTotal: visibleGraphProgress?.total ?? undefined,
|
||||
stale: false,
|
||||
|
||||
@@ -76,9 +76,7 @@ describe("OnboardingModelStep", () => {
|
||||
localStorage.setItem(LANGUAGE_STORAGE_KEY, locale);
|
||||
const title = guiMessages[locale]["配置记忆整理模型"];
|
||||
const back = guiMessages[locale]["返回选择模式"];
|
||||
const summary = guiMessages[locale]["记忆整理"];
|
||||
if (!title || !back || !summary)
|
||||
throw new Error(`missing ${locale} model copy`);
|
||||
if (!title || !back) throw new Error(`missing ${locale} model copy`);
|
||||
render(
|
||||
<LanguageProvider>
|
||||
<OnboardingModelStep {...props()} />
|
||||
@@ -95,11 +93,10 @@ describe("OnboardingModelStep", () => {
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByLabelText(/API Key/)).toBeTruthy();
|
||||
expect(screen.getByRole("complementary", { name: summary })).toBeTruthy();
|
||||
expect(screen.getAllByRole("button").length).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
|
||||
it("keeps current behavior in the restored open-source composition", () => {
|
||||
it("keeps step 3 on memory mode in a generation-only column", () => {
|
||||
render(<OnboardingModelStep {...props()} />);
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "配置记忆整理模型" }),
|
||||
@@ -113,9 +110,12 @@ describe("OnboardingModelStep", () => {
|
||||
expect(screen.getByText("DeepSeek · deepseek-flash")).toBeTruthy();
|
||||
expect(screen.queryByText("本次沉淀")).toBeNull();
|
||||
expect(screen.queryByLabelText(/向量|Embedding/)).toBeNull();
|
||||
expect(document.querySelector(".onboarding-model-layout")).toBeTruthy();
|
||||
expect(document.querySelector(".onboarding-model-summary")).toBeTruthy();
|
||||
expect(screen.getByText("让记忆从这里开始")).toBeTruthy();
|
||||
expect(
|
||||
document.querySelector(
|
||||
".onboarding-model-layout--single-column-600" +
|
||||
'[data-responsive="single-column-600"]',
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("focuses the page title when entering the model substep", async () => {
|
||||
@@ -181,18 +181,6 @@ describe("OnboardingModelStep", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("returns to the visible API key field when it fails in advanced mode", async () => {
|
||||
const p = props();
|
||||
p.advancedOpen = true;
|
||||
p.fieldErrors = { api_key: "请填写 API Key。" };
|
||||
const view = render(<OnboardingModelStep {...p} />);
|
||||
await waitFor(() => expect(p.onAdvancedChange).toHaveBeenCalledWith(false));
|
||||
view.rerender(<OnboardingModelStep {...p} advancedOpen={false} />);
|
||||
await waitFor(() =>
|
||||
expect(document.activeElement).toBe(screen.getByLabelText("API Key")),
|
||||
);
|
||||
});
|
||||
|
||||
it("links field errors and keeps secrets out of callback errors", async () => {
|
||||
const p = props();
|
||||
p.fieldErrors = { api_key: "请填写 API Key。" };
|
||||
|
||||
@@ -12,7 +12,6 @@ import type {
|
||||
import { useOnboardingTranslation } from "./onboarding-language";
|
||||
import { R6ColdStartFrame } from "./R6ColdStartFrame";
|
||||
import { R6Button } from "./R6Controls";
|
||||
import "./onboarding-model-r6.css";
|
||||
|
||||
type Action = "test" | "save-and-enable";
|
||||
|
||||
@@ -61,10 +60,6 @@ export function OnboardingModelStepNext(props: OnboardingModelStepProps) {
|
||||
props.onAdvancedChange(true);
|
||||
return;
|
||||
}
|
||||
if (firstError === "api_key" && props.advancedOpen) {
|
||||
props.onAdvancedChange(false);
|
||||
return;
|
||||
}
|
||||
if (firstError && (!technicalError || props.advancedOpen)) {
|
||||
formRef.current
|
||||
?.querySelector<HTMLElement>(`[name="${firstError}"]`)
|
||||
@@ -178,7 +173,10 @@ export function OnboardingModelStepNext(props: OnboardingModelStepProps) {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="onboarding-model-layout">
|
||||
<div
|
||||
className="onboarding-model-layout onboarding-model-layout--single-column-600"
|
||||
data-responsive="single-column-600"
|
||||
>
|
||||
<section
|
||||
ref={formRef}
|
||||
className="onboarding-model-form"
|
||||
@@ -186,9 +184,6 @@ export function OnboardingModelStepNext(props: OnboardingModelStepProps) {
|
||||
data-advanced={props.advancedOpen}
|
||||
>
|
||||
<div className="onboarding-model-group">
|
||||
<p className="onboarding-model-intro">
|
||||
{text("选择服务商,填入 API Key。核对已有配置后再继续。")}
|
||||
</p>
|
||||
<label
|
||||
className="onboarding-model-field onboarding-model-provider"
|
||||
htmlFor={`${id}-provider`}
|
||||
@@ -268,6 +263,16 @@ export function OnboardingModelStepNext(props: OnboardingModelStepProps) {
|
||||
)}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="onboarding-model-disclosure">
|
||||
<p>
|
||||
{text("相关对话会发送给所选服务商。")}
|
||||
{sourceSummary ? ` ${sourceSummary}` : ""}
|
||||
</p>
|
||||
<p>
|
||||
{text("密钥保存在系统安全存储中,不写入对话或日志。")}{" "}
|
||||
{text("测试只向此接口发送测试内容,不发送历史对话。")}
|
||||
</p>
|
||||
</div>
|
||||
<GenerationTechnicalDisclosure
|
||||
open={props.advancedOpen}
|
||||
disabled={fieldsLocked}
|
||||
@@ -343,35 +348,6 @@ export function OnboardingModelStepNext(props: OnboardingModelStepProps) {
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
<aside
|
||||
className="onboarding-model-summary"
|
||||
aria-label={text("记忆整理")}
|
||||
>
|
||||
<div className="onboarding-model-summary-heading">
|
||||
<p>{text("记忆整理")}</p>
|
||||
<h2>{text("让记忆从这里开始")}</h2>
|
||||
</div>
|
||||
<div className="onboarding-model-connections">
|
||||
<p>{text("对话来源")}</p>
|
||||
<strong>{sourceSummary || text("尚无已选来源")}</strong>
|
||||
<small>
|
||||
{t("onboardingImportedConversations", {
|
||||
count: props.importedConversationCount ?? 0,
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
<div className="onboarding-model-summary-status">
|
||||
<small>
|
||||
{text("处理服务:")}
|
||||
{text(provider?.label ?? "待选择")}
|
||||
</small>
|
||||
<small>
|
||||
{text("相关对话会发送给所选服务商。")}
|
||||
<br />
|
||||
{text("测试只向此接口发送测试内容,不发送历史对话。")}
|
||||
</small>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</R6ColdStartFrame>
|
||||
</div>
|
||||
|
||||
@@ -19,13 +19,23 @@ describe("R6 shared controls", () => {
|
||||
|
||||
it("keeps its label and prevents duplicate activation while pending", () => {
|
||||
const onClick = vi.fn();
|
||||
render(
|
||||
<R6Button pending onClick={onClick}>
|
||||
const view = render(
|
||||
<R6Button pending={false} onClick={onClick}>
|
||||
保存修改
|
||||
</R6Button>,
|
||||
);
|
||||
const button = screen.getByRole("button", { name: "保存修改" });
|
||||
const reservedSpinner = button.querySelector(".r6-button-spinner");
|
||||
expect(reservedSpinner).toBeTruthy();
|
||||
expect(reservedSpinner?.getAttribute("data-visible")).toBeNull();
|
||||
view.rerender(
|
||||
<R6Button pending onClick={onClick}>
|
||||
保存修改
|
||||
</R6Button>,
|
||||
);
|
||||
expect(button.getAttribute("aria-busy")).toBe("true");
|
||||
expect(button.querySelector(".r6-button-spinner")).toBe(reservedSpinner);
|
||||
expect(reservedSpinner?.getAttribute("data-visible")).toBe("true");
|
||||
fireEvent.click(button);
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
expect(button.textContent).toContain("保存修改");
|
||||
|
||||
@@ -13,24 +13,32 @@ type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
|
||||
export function R6Button({
|
||||
variant = "secondary",
|
||||
pending = false,
|
||||
pending,
|
||||
disabled,
|
||||
children,
|
||||
className = "",
|
||||
type = "button",
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const supportsPending = pending !== undefined;
|
||||
const busy = pending === true;
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
type={type}
|
||||
className={`r6-button ${className}`}
|
||||
data-variant={variant}
|
||||
disabled={disabled || pending}
|
||||
aria-busy={pending || undefined}
|
||||
disabled={disabled || busy}
|
||||
aria-busy={busy || undefined}
|
||||
>
|
||||
<span className="r6-button-label">{children}</span>
|
||||
{pending && <span className="r6-button-spinner" aria-hidden="true" />}
|
||||
{supportsPending ? (
|
||||
<span
|
||||
className="r6-button-spinner"
|
||||
data-visible={busy || undefined}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,17 @@
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.r6-evolve-status {
|
||||
width: fit-content;
|
||||
margin: 12px 0 -4px;
|
||||
padding: 5px 9px;
|
||||
border: 1px solid #cbd8e0;
|
||||
border-radius: 999px;
|
||||
background: #edf3f6;
|
||||
color: #4f6574;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.r6-home-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: 1.1fr 1fr 1fr;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { EvolveOverview, EvolveRunReceipt } from "@one-memory/contracts";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { getEvolveOverview } from "./api";
|
||||
import {
|
||||
commitEvolveRun,
|
||||
localEvolveRunning,
|
||||
refreshEvolveOverview,
|
||||
} from "./evolve-state";
|
||||
import { queryClient, resetClient } from "./query-client";
|
||||
|
||||
vi.mock("./api", () => ({ getEvolveOverview: vi.fn() }));
|
||||
|
||||
const receipt: EvolveRunReceipt = {
|
||||
run_id: "run-1",
|
||||
user_id: "local",
|
||||
agent_id: "all-apps",
|
||||
max_batches: null,
|
||||
trigger: "manual",
|
||||
state: "running",
|
||||
started_at: "2026-11-05T14:00:00Z",
|
||||
finished_at: null,
|
||||
rounds: 0,
|
||||
tasks_processed: 0,
|
||||
batch_failures: 0,
|
||||
cycle_status: null,
|
||||
should_continue: null,
|
||||
continuation_reason: null,
|
||||
remaining_clusters: null,
|
||||
remaining_actionable_clusters: null,
|
||||
remaining_fresh_facts: null,
|
||||
capped: false,
|
||||
elapsed_ms: 0,
|
||||
error_code: null,
|
||||
failure_kind: null,
|
||||
request_id: "request-1",
|
||||
};
|
||||
|
||||
const overview: EvolveOverview = {
|
||||
mode: "ultra",
|
||||
schedule: { enabled: true, time: "03:00" },
|
||||
users: {},
|
||||
running: [],
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
resetClient();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("shares the latest server overview through the query cache", async () => {
|
||||
vi.mocked(getEvolveOverview).mockResolvedValue(overview);
|
||||
|
||||
await expect(refreshEvolveOverview()).resolves.toEqual(overview);
|
||||
expect(queryClient.getQueryData(["evolve", "local"])).toEqual(overview);
|
||||
});
|
||||
|
||||
it("publishes an accepted local run immediately from a cold cache", () => {
|
||||
commitEvolveRun(receipt, overview);
|
||||
|
||||
const current = queryClient.getQueryData<EvolveOverview>(["evolve", "local"]);
|
||||
expect(current?.running).toEqual([receipt]);
|
||||
expect(localEvolveRunning(current)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not let a pre-mutation overview overwrite the accepted run", async () => {
|
||||
let resolveRead: ((value: EvolveOverview) => void) | undefined;
|
||||
vi.mocked(getEvolveOverview).mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
resolveRead = resolve;
|
||||
}),
|
||||
);
|
||||
const staleRead = refreshEvolveOverview();
|
||||
|
||||
commitEvolveRun(receipt, overview);
|
||||
resolveRead?.(overview);
|
||||
|
||||
await expect(staleRead).resolves.toMatchObject({ running: [receipt] });
|
||||
expect(
|
||||
queryClient.getQueryData<EvolveOverview>(["evolve", "local"])?.running,
|
||||
).toEqual([receipt]);
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { EvolveOverview, EvolveRunReceipt } from "@one-memory/contracts";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import * as api from "./api";
|
||||
import { onClientReset, queryClient } from "./query-client";
|
||||
|
||||
const EVOLVE_KEY = ["evolve", "local"] as const;
|
||||
|
||||
queryClient.setQueryDefaults(EVOLVE_KEY, {
|
||||
gcTime: Number.POSITIVE_INFINITY,
|
||||
staleTime: 1_000,
|
||||
});
|
||||
|
||||
let mutationGeneration = 0;
|
||||
let readsStarted = 0;
|
||||
let readsLanded = 0;
|
||||
onClientReset(() => {
|
||||
mutationGeneration = 0;
|
||||
readsStarted = 0;
|
||||
readsLanded = 0;
|
||||
});
|
||||
|
||||
export async function refreshEvolveOverview() {
|
||||
const generation = mutationGeneration;
|
||||
const order = ++readsStarted;
|
||||
const next = await api.getEvolveOverview();
|
||||
const current = queryClient.getQueryData<EvolveOverview>(EVOLVE_KEY);
|
||||
if (generation !== mutationGeneration || order < readsLanded)
|
||||
return current ?? next;
|
||||
readsLanded = order;
|
||||
queryClient.setQueryData(EVOLVE_KEY, next);
|
||||
return next;
|
||||
}
|
||||
|
||||
/** Publish an accepted run before polling confirms it so every page agrees. */
|
||||
export function commitEvolveRun(
|
||||
receipt: EvolveRunReceipt,
|
||||
fallback?: EvolveOverview,
|
||||
) {
|
||||
mutationGeneration++;
|
||||
queryClient.setQueryData<EvolveOverview>(EVOLVE_KEY, (current) => {
|
||||
const base = current ?? fallback;
|
||||
return base
|
||||
? {
|
||||
...base,
|
||||
running: [
|
||||
...base.running.filter(
|
||||
(running) => running.user_id !== receipt.user_id,
|
||||
),
|
||||
receipt,
|
||||
],
|
||||
}
|
||||
: current;
|
||||
});
|
||||
void queryClient.invalidateQueries({ queryKey: EVOLVE_KEY });
|
||||
}
|
||||
|
||||
export function useEvolveState(active = true) {
|
||||
return useQuery(
|
||||
{
|
||||
queryKey: EVOLVE_KEY,
|
||||
queryFn: refreshEvolveOverview,
|
||||
enabled: active,
|
||||
refetchInterval: (query) =>
|
||||
query.state.data?.running.length ? 1_500 : 10_000,
|
||||
},
|
||||
queryClient,
|
||||
);
|
||||
}
|
||||
|
||||
export function useEvolveOverview(active = true) {
|
||||
return useEvolveState(active).data;
|
||||
}
|
||||
|
||||
export function localEvolveRunning(overview?: EvolveOverview | null) {
|
||||
return overview?.running.some((run) => run.user_id === "local") ?? false;
|
||||
}
|
||||
@@ -588,44 +588,51 @@
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* The current controller keeps generation-only behavior. The presentation
|
||||
intentionally returns to the original open-source setup composition:
|
||||
an open form column, a separate context column, and a distinct advanced state. */
|
||||
.onboarding-model-group {
|
||||
/* Generation-only cold start. Embedding remains a separate settings lane. */
|
||||
.onboarding-model-r6 .onboarding-r6-head > div {
|
||||
width: min(760px, 100%);
|
||||
}
|
||||
.onboarding-model-layout {
|
||||
display: block;
|
||||
width: min(880px, 100%);
|
||||
height: auto;
|
||||
margin-inline: auto;
|
||||
}
|
||||
.onboarding-model-group > .onboarding-model-intro {
|
||||
margin-bottom: 0;
|
||||
.onboarding-model-form {
|
||||
overflow: visible;
|
||||
}
|
||||
.onboarding-model-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18px 20px;
|
||||
padding: 24px 26px;
|
||||
border: 1px solid #d8e2e8;
|
||||
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 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.onboarding-model-field {
|
||||
gap: 7px;
|
||||
}
|
||||
.onboarding-model-field :is(input, select),
|
||||
.onboarding-model-field .om-option-menu-button,
|
||||
.generation-model-field :is(input, select) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 54px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #c0ccd5;
|
||||
height: 44px;
|
||||
padding: 9px 12px;
|
||||
border: 1px solid #c8d3db;
|
||||
border-radius: 8px;
|
||||
background: #f0f5f6;
|
||||
background: #fff;
|
||||
color: #262a2e;
|
||||
font:
|
||||
400 14px / 23px "Noto Sans SC",
|
||||
400 14px / 22px "Noto Sans SC",
|
||||
"PingFang SC",
|
||||
sans-serif;
|
||||
transition:
|
||||
border-color var(--r6-fast, 140ms),
|
||||
background-color var(--r6-fast, 140ms);
|
||||
}
|
||||
.onboarding-model-field .om-option-menu-button:hover:not(:disabled),
|
||||
.generation-model-field :is(input, select):hover:not(:disabled) {
|
||||
border-color: #9fb6ad;
|
||||
background: #eaf1f2;
|
||||
}
|
||||
.generation-model-field select:not(:disabled),
|
||||
.generation-technical-toggle:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
:is(.onboarding-model-field, .generation-model-field) [aria-invalid="true"] {
|
||||
border-color: #a24646;
|
||||
}
|
||||
.onboarding-model-field small,
|
||||
.generation-model-field small {
|
||||
@@ -634,10 +641,14 @@
|
||||
line-height: 19px;
|
||||
}
|
||||
.onboarding-model-default-summary {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-height: 54px;
|
||||
margin-top: 12px;
|
||||
padding: 14px 0;
|
||||
border-block: 1px solid #e4eaee;
|
||||
}
|
||||
.onboarding-model-default-summary span,
|
||||
.onboarding-model-disclosure {
|
||||
color: #63707b;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
@@ -647,131 +658,154 @@
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.onboarding-model-disclosure {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
.generation-technical-disclosure {
|
||||
margin-top: 23px;
|
||||
grid-column: 1 / -1;
|
||||
border-top: 1px solid #e4eaee;
|
||||
padding-top: 4px;
|
||||
}
|
||||
.generation-technical-toggle {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 28px;
|
||||
min-height: 44px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
padding: 8px 0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #4e6478;
|
||||
font:
|
||||
500 13px / 21px "Noto Sans SC",
|
||||
"PingFang SC",
|
||||
sans-serif;
|
||||
transition:
|
||||
background-color var(--r6-fast, 140ms),
|
||||
color var(--r6-fast, 140ms);
|
||||
}
|
||||
.generation-technical-toggle:hover:not(:disabled) {
|
||||
background: #ffffff66;
|
||||
color: #33424d;
|
||||
}
|
||||
.generation-technical-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 19px 30px;
|
||||
margin-top: 34px;
|
||||
}
|
||||
.generation-technical-fields[hidden] {
|
||||
display: none;
|
||||
gap: 16px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.generation-model-field {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 5px;
|
||||
gap: 6px;
|
||||
color: #63707b;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.generation-model-field:has([name="base_url"]),
|
||||
.generation-model-field:has([name="base-url"]),
|
||||
.generation-runtime-fields,
|
||||
.generation-runtime-note,
|
||||
.generation-thinking {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.generation-model-field :is(input, select) {
|
||||
height: 42px;
|
||||
padding: 9px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
}
|
||||
.generation-runtime-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 24px;
|
||||
gap: 12px;
|
||||
}
|
||||
.generation-technical-fields[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.generation-runtime-note {
|
||||
grid-column: 1 / -1;
|
||||
color: #63707b;
|
||||
font-size: 12px;
|
||||
line-height: 19px;
|
||||
}
|
||||
.generation-thinking {
|
||||
display: flex;
|
||||
min-height: 28px;
|
||||
min-height: 32px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 9px;
|
||||
color: #416e5c;
|
||||
font-size: 14px;
|
||||
line-height: 23px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.onboarding-model-r6 .generation-model-error {
|
||||
color: #a24646;
|
||||
}
|
||||
.onboarding-model-form[data-advanced="true"]
|
||||
:is(
|
||||
.onboarding-model-intro,
|
||||
.onboarding-model-provider,
|
||||
.onboarding-model-key,
|
||||
.onboarding-model-default-summary
|
||||
) {
|
||||
display: none;
|
||||
.onboarding-model-recovery-actions {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.onboarding-model-form[data-advanced="true"] .generation-technical-disclosure {
|
||||
margin-top: 0;
|
||||
}
|
||||
.onboarding-model-form[data-advanced="true"] .generation-technical-toggle {
|
||||
min-height: 28px;
|
||||
}
|
||||
.onboarding-model-recovery-actions,
|
||||
.onboarding-model-feedback {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.onboarding-model-r6 .onboarding-r6-foot {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 20px 32px;
|
||||
}
|
||||
.onboarding-model-r6 .onboarding-model-footer-actions {
|
||||
flex-wrap: nowrap;
|
||||
margin-left: 0;
|
||||
}
|
||||
.onboarding-model-r6 .onboarding-model-footer-actions .r6-button {
|
||||
min-width: 132px;
|
||||
}
|
||||
@media (min-width: 761px) {
|
||||
.onboarding-model-group {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
.onboarding-model-r6 .onboarding-r6-body {
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-gutter: stable;
|
||||
scroll-padding: 12px;
|
||||
padding-inline: 4px 16px;
|
||||
}
|
||||
.onboarding-model-form {
|
||||
display: block;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
@media (min-width: 761px) and (max-height: 959px) {
|
||||
.onboarding-model-form[data-advanced="false"]
|
||||
.onboarding-model-field
|
||||
:is(input, select, .om-option-menu-button) {
|
||||
height: 44px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
.onboarding-model-r6 .onboarding-r6-body {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.generation-technical-fields {
|
||||
margin-top: 16px;
|
||||
.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: 760px) {
|
||||
.generation-technical-fields,
|
||||
.generation-runtime-fields {
|
||||
@media (max-width: 600px) {
|
||||
.onboarding-model-layout--single-column-600 .onboarding-model-group {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.onboarding-model-layout--single-column-600 .generation-technical-fields,
|
||||
.onboarding-model-layout--single-column-600 .generation-runtime-fields {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.onboarding-model-footer-actions .r6-button,
|
||||
.onboarding-model-footer-actions .r6-button[data-variant="primary"] {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.onboarding-model-r6 .onboarding-r6-foot {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.onboarding-model-r6 .onboarding-model-footer-actions {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.onboarding-model-r6 .onboarding-model-footer-actions .r6-button {
|
||||
flex: 1 1 160px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.onboarding-model-r6 .onboarding-model-footer-actions .r6-button,
|
||||
.onboarding-model-r6
|
||||
.onboarding-model-footer-actions
|
||||
.r6-button[data-variant="primary"] {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -159,7 +159,7 @@ button.r6-button {
|
||||
min-height: 44px;
|
||||
height: 44px;
|
||||
padding: 0 20px;
|
||||
gap: 0;
|
||||
gap: 8px;
|
||||
border: 1px solid var(--r6-control-edge);
|
||||
border-radius: 8px;
|
||||
background: rgb(255 255 255 / 20%);
|
||||
@@ -244,14 +244,20 @@ button.r6-button[data-variant="quiet"]:disabled {
|
||||
min-width: 0;
|
||||
}
|
||||
.r6-button-spinner {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
position: static;
|
||||
flex: 0 0 auto;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 1.5px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
visibility: hidden;
|
||||
animation: r6-spin 1.8s linear infinite;
|
||||
animation-play-state: paused;
|
||||
}
|
||||
.r6-button-spinner[data-visible="true"] {
|
||||
visibility: visible;
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
.r6-stepper {
|
||||
|
||||
@@ -33,6 +33,17 @@
|
||||
font-size: 13px;
|
||||
color: #637786;
|
||||
}
|
||||
.r6-memory .memory-evolve-status {
|
||||
width: fit-content;
|
||||
margin: -12px 0 20px;
|
||||
padding: 5px 9px;
|
||||
border: 1px solid #cbd8e0;
|
||||
border-radius: 999px;
|
||||
background: #edf3f6;
|
||||
color: #4f6574;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.r6-memory .memory-pending-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in new issue
Block a user