Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39c20cb5fe | ||
|
|
926a208844 | ||
|
|
b7731ac834 | ||
|
|
9560327ee2 | ||
|
|
f445ad69c4 |
No files matched your search
@@ -0,0 +1,70 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { LanguageProvider } from "./gui-language";
|
||||
import { MemoryModeCards } from "./MemoryModeCards";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
it("shows every mode feature and reveals Ultra scheduling after selection", async () => {
|
||||
const onChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
const view = (value: "lite" | "pro" | "ultra") => (
|
||||
<LanguageProvider>
|
||||
<MemoryModeCards
|
||||
aria-label="记忆模式"
|
||||
value={value}
|
||||
layout="rows"
|
||||
onChange={onChange}
|
||||
ultraDetail={
|
||||
<div>
|
||||
<strong>每天自动整合</strong>
|
||||
<small>默认 03:00,可在设置中修改。</small>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</LanguageProvider>
|
||||
);
|
||||
const { rerender } = render(view("pro"));
|
||||
|
||||
expect(screen.getByRole("radiogroup", { name: "记忆模式" })).toBeTruthy();
|
||||
expect(screen.getAllByRole("radio")).toHaveLength(3);
|
||||
expect(screen.getByText("不调用生成模型")).toBeTruthy();
|
||||
expect(screen.getByText("沉淀高级记忆")).toBeTruthy();
|
||||
expect(screen.getByText("增强关联召回")).toBeTruthy();
|
||||
expect(screen.queryByText("每天自动整合")).toBeNull();
|
||||
|
||||
await user.click(screen.getByRole("radio", { name: /深度记忆/ }));
|
||||
expect(onChange).toHaveBeenCalledWith("ultra", expect.any(HTMLInputElement));
|
||||
|
||||
rerender(view("ultra"));
|
||||
expect(screen.getByText("每天自动整合")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("preserves per-mode disabled behavior", () => {
|
||||
render(
|
||||
<LanguageProvider>
|
||||
<MemoryModeCards
|
||||
aria-label="记忆模式"
|
||||
value="lite"
|
||||
disabled={(mode) => mode !== "lite"}
|
||||
onChange={vi.fn()}
|
||||
/>
|
||||
</LanguageProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("radio", { name: /基础记忆/ })).toHaveProperty(
|
||||
"disabled",
|
||||
false,
|
||||
);
|
||||
expect(screen.getByRole("radio", { name: /智能记忆/ })).toHaveProperty(
|
||||
"disabled",
|
||||
true,
|
||||
);
|
||||
expect(screen.getByRole("radio", { name: /深度记忆/ })).toHaveProperty(
|
||||
"disabled",
|
||||
true,
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MemoryPipelineMode } from "@one-memory/contracts";
|
||||
import { Check } from "lucide-react";
|
||||
import { Check, Clock3 } from "lucide-react";
|
||||
import { type ReactNode, useId } from "react";
|
||||
import { MODE_COPY } from "./memory-mode-ui";
|
||||
import { useOnboardingTranslation } from "./onboarding-language";
|
||||
@@ -32,6 +32,7 @@ export function MemoryModeCards({
|
||||
current,
|
||||
currentBadge,
|
||||
disabled = false,
|
||||
layout = "grid",
|
||||
onChange,
|
||||
ultraDetail,
|
||||
"aria-label": ariaLabel,
|
||||
@@ -41,6 +42,7 @@ export function MemoryModeCards({
|
||||
current?: MemoryPipelineMode;
|
||||
currentBadge?: ReactNode;
|
||||
disabled?: boolean | ((mode: MemoryPipelineMode) => boolean);
|
||||
layout?: "grid" | "rows";
|
||||
onChange(mode: MemoryPipelineMode, control: HTMLInputElement): void;
|
||||
ultraDetail?: ReactNode;
|
||||
"aria-label": string;
|
||||
@@ -53,70 +55,77 @@ export function MemoryModeCards({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="memory-mode-cards"
|
||||
className={`memory-mode-cards memory-mode-cards--${layout}`}
|
||||
role="radiogroup"
|
||||
aria-label={ariaLabel}
|
||||
aria-busy={ariaBusy || undefined}
|
||||
>
|
||||
{MODES.map((mode) => {
|
||||
const selected = value === mode.id;
|
||||
const modeDisabled = unavailable(mode.id);
|
||||
const localizedTitle = text(MODE_COPY[mode.id].title);
|
||||
const title = localizedTitle.replace(
|
||||
/\s*[((](?:lite|pro|ultra)[))]\s*$/i,
|
||||
"",
|
||||
);
|
||||
const features = text(MODE_COPY[mode.id].description).split(/\s*·\s*/);
|
||||
return (
|
||||
<div
|
||||
key={mode.id}
|
||||
className="memory-mode-card"
|
||||
data-mode={mode.id}
|
||||
data-selected={selected || undefined}
|
||||
data-current={current === mode.id || undefined}
|
||||
data-disabled={modeDisabled || undefined}
|
||||
>
|
||||
<label className="memory-mode-card-choice">
|
||||
<input
|
||||
type="radio"
|
||||
name={name}
|
||||
value={mode.id}
|
||||
aria-label={localizedTitle}
|
||||
checked={selected}
|
||||
disabled={modeDisabled}
|
||||
onChange={(event) => onChange(mode.id, event.currentTarget)}
|
||||
/>
|
||||
<span className="memory-mode-title">
|
||||
<span className="memory-mode-index">{mode.index}</span>
|
||||
<strong>{title}</strong>
|
||||
<span className="memory-mode-tier">
|
||||
{mode.id.toUpperCase()}
|
||||
</span>
|
||||
{current === mode.id && currentBadge ? (
|
||||
<span className="memory-mode-current">{currentBadge}</span>
|
||||
) : null}
|
||||
<span className="memory-mode-radio" aria-hidden="true">
|
||||
{selected ? <Check size={13} /> : null}
|
||||
</span>
|
||||
</span>
|
||||
<span className="memory-mode-description">
|
||||
{text(mode.description)}
|
||||
</span>
|
||||
<span className="memory-mode-features">
|
||||
{features.map((feature) => (
|
||||
<span key={feature}>
|
||||
<i aria-hidden="true" />
|
||||
{text(feature)}
|
||||
<div className="memory-mode-card-list">
|
||||
{MODES.map((mode) => {
|
||||
const selected = value === mode.id;
|
||||
const modeDisabled = unavailable(mode.id);
|
||||
const localizedTitle = text(MODE_COPY[mode.id].title);
|
||||
const title = localizedTitle.replace(
|
||||
/\s*[((](?:lite|pro|ultra)[))]\s*$/i,
|
||||
"",
|
||||
);
|
||||
const features = text(MODE_COPY[mode.id].description).split(
|
||||
/\s*·\s*/,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={mode.id}
|
||||
className="memory-mode-card"
|
||||
data-mode={mode.id}
|
||||
data-selected={selected || undefined}
|
||||
data-current={current === mode.id || undefined}
|
||||
data-disabled={modeDisabled || undefined}
|
||||
>
|
||||
<label className="memory-mode-card-choice">
|
||||
<input
|
||||
type="radio"
|
||||
name={name}
|
||||
value={mode.id}
|
||||
aria-label={localizedTitle}
|
||||
checked={selected}
|
||||
disabled={modeDisabled}
|
||||
onChange={(event) => onChange(mode.id, event.currentTarget)}
|
||||
/>
|
||||
<span className="memory-mode-title">
|
||||
<span className="memory-mode-index">{mode.index}</span>
|
||||
<strong>{title}</strong>
|
||||
<span className="memory-mode-tier">
|
||||
{mode.id.toUpperCase()}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</label>
|
||||
{mode.id === "ultra" && selected && ultraDetail ? (
|
||||
<div className="memory-mode-detail">{ultraDetail}</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{current === mode.id && currentBadge ? (
|
||||
<span className="memory-mode-current">{currentBadge}</span>
|
||||
) : null}
|
||||
<span className="memory-mode-radio" aria-hidden="true">
|
||||
{selected ? <Check size={13} /> : null}
|
||||
</span>
|
||||
</span>
|
||||
<span className="memory-mode-description">
|
||||
{text(mode.description)}
|
||||
</span>
|
||||
<span className="memory-mode-features">
|
||||
{features.map((feature) => (
|
||||
<span key={feature}>
|
||||
<i aria-hidden="true" />
|
||||
{text(feature)}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
{mode.id === "ultra" && selected && ultraDetail ? (
|
||||
<div className="memory-mode-detail">
|
||||
<Clock3 size={14} aria-hidden="true" />
|
||||
<div>{ultraDetail}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1266,6 +1266,7 @@ export function OnboardingFlow({
|
||||
<MemoryModeCards
|
||||
aria-label={text("记忆模式")}
|
||||
value={modeChoice}
|
||||
layout="rows"
|
||||
disabled={modeSaving || completionBusy}
|
||||
onChange={(mode) => {
|
||||
modeChoiceTouched.current = true;
|
||||
|
||||
@@ -76,7 +76,9 @@ describe("OnboardingModelStep", () => {
|
||||
localStorage.setItem(LANGUAGE_STORAGE_KEY, locale);
|
||||
const title = guiMessages[locale]["配置记忆整理模型"];
|
||||
const back = guiMessages[locale]["返回选择模式"];
|
||||
if (!title || !back) throw new Error(`missing ${locale} model copy`);
|
||||
const summary = guiMessages[locale]["记忆整理"];
|
||||
if (!title || !back || !summary)
|
||||
throw new Error(`missing ${locale} model copy`);
|
||||
render(
|
||||
<LanguageProvider>
|
||||
<OnboardingModelStep {...props()} />
|
||||
@@ -93,10 +95,11 @@ 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 step 3 on memory mode in a generation-only column", () => {
|
||||
it("keeps current behavior in the restored open-source composition", () => {
|
||||
render(<OnboardingModelStep {...props()} />);
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "配置记忆整理模型" }),
|
||||
@@ -110,12 +113,9 @@ describe("OnboardingModelStep", () => {
|
||||
expect(screen.getByText("DeepSeek · deepseek-flash")).toBeTruthy();
|
||||
expect(screen.queryByText("本次沉淀")).toBeNull();
|
||||
expect(screen.queryByLabelText(/向量|Embedding/)).toBeNull();
|
||||
expect(
|
||||
document.querySelector(
|
||||
".onboarding-model-layout--single-column-600" +
|
||||
'[data-responsive="single-column-600"]',
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(document.querySelector(".onboarding-model-layout")).toBeTruthy();
|
||||
expect(document.querySelector(".onboarding-model-summary")).toBeTruthy();
|
||||
expect(screen.getByText("让记忆从这里开始")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("focuses the page title when entering the model substep", async () => {
|
||||
@@ -181,6 +181,18 @@ 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,6 +12,7 @@ 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";
|
||||
|
||||
@@ -60,6 +61,10 @@ 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}"]`)
|
||||
@@ -173,10 +178,7 @@ export function OnboardingModelStepNext(props: OnboardingModelStepProps) {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="onboarding-model-layout onboarding-model-layout--single-column-600"
|
||||
data-responsive="single-column-600"
|
||||
>
|
||||
<div className="onboarding-model-layout">
|
||||
<section
|
||||
ref={formRef}
|
||||
className="onboarding-model-form"
|
||||
@@ -184,6 +186,9 @@ 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`}
|
||||
@@ -263,16 +268,6 @@ 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}
|
||||
@@ -348,6 +343,35 @@ 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>
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
.memory-mode-cards {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
min-height: 420px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.memory-mode-card-list {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.memory-mode-card {
|
||||
position: relative;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 286px;
|
||||
min-height: 128px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -43,10 +55,15 @@
|
||||
}
|
||||
|
||||
.memory-mode-card-choice {
|
||||
min-height: 284px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
padding: 18px 20px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(230px, 0.85fr) minmax(0, 1.45fr);
|
||||
grid-template-rows: auto auto minmax(0, 1fr);
|
||||
column-gap: 28px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -68,10 +85,13 @@
|
||||
}
|
||||
|
||||
.memory-mode-title {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-right: 34px;
|
||||
}
|
||||
|
||||
.memory-mode-index {
|
||||
@@ -110,9 +130,11 @@
|
||||
}
|
||||
|
||||
.memory-mode-radio {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 20px;
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
margin-left: auto;
|
||||
flex: 0 0 21px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -133,8 +155,11 @@
|
||||
}
|
||||
|
||||
.memory-mode-description {
|
||||
min-height: 50px;
|
||||
margin-top: 10px;
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
align-self: start;
|
||||
min-height: 0;
|
||||
margin-top: 6px;
|
||||
color: #526674;
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
@@ -142,11 +167,16 @@
|
||||
}
|
||||
|
||||
.memory-mode-features {
|
||||
margin-top: auto;
|
||||
padding: 16px 0 2px;
|
||||
grid-column: 2;
|
||||
grid-row: 1 / -1;
|
||||
align-self: stretch;
|
||||
margin-top: 0;
|
||||
padding: 0 42px 0 28px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
align-content: center;
|
||||
gap: 8px;
|
||||
border-top: 1px solid #dbe4ea;
|
||||
border-left: 1px solid #dbe4ea;
|
||||
color: #5e6e7a;
|
||||
font-size: 12px;
|
||||
line-height: 19px;
|
||||
@@ -168,14 +198,97 @@
|
||||
}
|
||||
|
||||
.memory-mode-detail {
|
||||
margin-top: auto;
|
||||
padding: 14px 18px;
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
align-self: start;
|
||||
margin-top: 6px;
|
||||
padding: 7px 0 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 7px;
|
||||
border-top: 1px solid #d4e0e7;
|
||||
background: #f1f6f8;
|
||||
background: transparent;
|
||||
color: #405867;
|
||||
font-size: 11px;
|
||||
line-height: 17px;
|
||||
animation: memory-mode-detail-in 180ms ease-out both;
|
||||
}
|
||||
|
||||
.memory-mode-detail > svg {
|
||||
margin-top: 1px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.memory-mode-detail > div {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid {
|
||||
flex: none;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-card-list {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-card {
|
||||
position: static;
|
||||
flex: none;
|
||||
min-height: 286px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-card-choice {
|
||||
position: static;
|
||||
min-height: 284px;
|
||||
height: auto;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-title {
|
||||
width: 100%;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-radio {
|
||||
position: static;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-description {
|
||||
min-height: 50px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-features {
|
||||
width: 100%;
|
||||
margin-top: auto;
|
||||
padding: 16px 0 2px;
|
||||
border-top: 1px solid #dbe4ea;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-detail {
|
||||
width: calc(100% + 40px);
|
||||
margin: auto -20px -20px;
|
||||
padding: 14px 18px;
|
||||
display: block;
|
||||
border-radius: 0 0 15px 15px;
|
||||
background: #f1f6f8;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
animation: memory-mode-detail-in 180ms ease-out both;
|
||||
}
|
||||
|
||||
.memory-mode-cards--grid .memory-mode-detail > svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes memory-mode-detail-in {
|
||||
@@ -185,34 +298,131 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 681px) and (max-height: 959px) {
|
||||
.memory-mode-cards {
|
||||
gap: 14px;
|
||||
@media (min-width: 761px) and (max-height: 959px) {
|
||||
.memory-mode-cards--rows {
|
||||
min-height: 360px;
|
||||
}
|
||||
.memory-mode-card {
|
||||
min-height: 194px;
|
||||
.memory-mode-cards--rows .memory-mode-card-list {
|
||||
gap: 12px;
|
||||
}
|
||||
.memory-mode-card-choice {
|
||||
min-height: 194px;
|
||||
padding: 16px;
|
||||
.memory-mode-cards--rows .memory-mode-card {
|
||||
min-height: 112px;
|
||||
}
|
||||
.memory-mode-title strong {
|
||||
.memory-mode-cards--rows .memory-mode-card-choice {
|
||||
min-height: 0;
|
||||
padding: 13px 16px;
|
||||
grid-template-columns: minmax(205px, 0.82fr) minmax(0, 1.5fr);
|
||||
column-gap: 22px;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-radio {
|
||||
top: 13px;
|
||||
right: 16px;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-title strong {
|
||||
font-size: 17px;
|
||||
line-height: 25px;
|
||||
}
|
||||
.memory-mode-description {
|
||||
.memory-mode-cards--rows .memory-mode-description {
|
||||
min-height: 0;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-features {
|
||||
margin-top: 0;
|
||||
padding: 0 36px 0 22px;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
line-height: 17px;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-detail {
|
||||
margin-top: 5px;
|
||||
padding: 5px 0 0;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.memory-mode-cards--rows {
|
||||
flex: none;
|
||||
min-height: 0;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-card-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-card {
|
||||
flex: none;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-card-choice {
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-radio {
|
||||
position: static;
|
||||
margin-left: auto;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-title {
|
||||
width: 100%;
|
||||
padding-right: 0;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-description {
|
||||
min-height: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-features {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
padding: 16px 0 0;
|
||||
border-top: 1px solid #dbe4ea;
|
||||
border-left: 0;
|
||||
}
|
||||
.memory-mode-cards--rows .memory-mode-detail {
|
||||
width: calc(100% + 40px);
|
||||
margin: 16px -20px -20px;
|
||||
padding: 14px 20px 16px;
|
||||
border-radius: 0 0 15px 15px;
|
||||
background: #f1f6f8;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 681px) and (max-height: 959px) {
|
||||
.memory-mode-cards--grid {
|
||||
gap: 14px;
|
||||
}
|
||||
.memory-mode-cards--grid .memory-mode-card {
|
||||
min-height: 194px;
|
||||
}
|
||||
.memory-mode-cards--grid .memory-mode-card-choice {
|
||||
min-height: 194px;
|
||||
padding: 16px;
|
||||
}
|
||||
.memory-mode-cards--grid .memory-mode-title strong {
|
||||
font-size: 17px;
|
||||
line-height: 25px;
|
||||
}
|
||||
.memory-mode-cards--grid .memory-mode-description {
|
||||
min-height: 36px;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.memory-mode-features {
|
||||
.memory-mode-cards--grid .memory-mode-features {
|
||||
padding-top: 10px;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.memory-mode-detail {
|
||||
.memory-mode-cards--grid .memory-mode-detail {
|
||||
width: calc(100% + 32px);
|
||||
margin: auto -16px -16px;
|
||||
padding: 10px 14px;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
@@ -220,14 +430,14 @@
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.memory-mode-cards {
|
||||
.memory-mode-cards--grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.memory-mode-card,
|
||||
.memory-mode-card-choice {
|
||||
.memory-mode-cards--grid .memory-mode-card,
|
||||
.memory-mode-cards--grid .memory-mode-card-choice {
|
||||
min-height: 0;
|
||||
}
|
||||
.memory-mode-description {
|
||||
.memory-mode-cards--grid .memory-mode-description {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,51 +588,44 @@
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* 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-form {
|
||||
overflow: visible;
|
||||
}
|
||||
/* 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 {
|
||||
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%);
|
||||
display: block;
|
||||
}
|
||||
.onboarding-model-provider,
|
||||
.onboarding-model-key {
|
||||
margin-top: 0;
|
||||
.onboarding-model-group > .onboarding-model-intro {
|
||||
margin-bottom: 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: 44px;
|
||||
padding: 9px 12px;
|
||||
border: 1px solid #c8d3db;
|
||||
height: 54px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #c0ccd5;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
background: #f0f5f6;
|
||||
color: #262a2e;
|
||||
font:
|
||||
400 14px / 22px "Noto Sans SC",
|
||||
400 14px / 23px "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 {
|
||||
@@ -641,14 +634,10 @@
|
||||
line-height: 19px;
|
||||
}
|
||||
.onboarding-model-default-summary {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 14px 0;
|
||||
border-block: 1px solid #e4eaee;
|
||||
}
|
||||
.onboarding-model-default-summary span,
|
||||
.onboarding-model-disclosure {
|
||||
min-height: 54px;
|
||||
margin-top: 12px;
|
||||
color: #63707b;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
@@ -658,154 +647,131 @@
|
||||
font-size: 14px;
|
||||
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;
|
||||
margin-top: 23px;
|
||||
}
|
||||
.generation-technical-toggle {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
min-height: 28px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
padding: 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: 16px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.generation-model-field {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
color: #63707b;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.generation-model-field:has([name="base-url"]),
|
||||
.generation-runtime-fields,
|
||||
.generation-thinking {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.generation-runtime-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
gap: 19px 30px;
|
||||
margin-top: 34px;
|
||||
}
|
||||
.generation-technical-fields[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.generation-runtime-note {
|
||||
.generation-model-field {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 5px;
|
||||
color: #63707b;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.generation-runtime-note {
|
||||
color: #63707b;
|
||||
font-size: 12px;
|
||||
line-height: 19px;
|
||||
}
|
||||
.generation-thinking {
|
||||
display: flex;
|
||||
min-height: 32px;
|
||||
min-height: 28px;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
gap: 10px;
|
||||
color: #416e5c;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
line-height: 23px;
|
||||
}
|
||||
.onboarding-model-r6 .generation-model-error {
|
||||
color: #a24646;
|
||||
}
|
||||
.onboarding-model-recovery-actions {
|
||||
margin-top: 12px;
|
||||
.onboarding-model-form[data-advanced="true"]
|
||||
:is(
|
||||
.onboarding-model-intro,
|
||||
.onboarding-model-provider,
|
||||
.onboarding-model-key,
|
||||
.onboarding-model-default-summary
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
.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-r6 .onboarding-r6-body {
|
||||
.onboarding-model-group {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.onboarding-model-form {
|
||||
display: block;
|
||||
overflow: visible;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-gutter: stable;
|
||||
scroll-padding: 12px;
|
||||
padding-inline: 4px 16px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 761px) and (max-height: 959px) {
|
||||
.onboarding-model-r6 .onboarding-r6-body {
|
||||
overflow-y: hidden;
|
||||
.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-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;
|
||||
.generation-technical-fields {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.onboarding-model-layout--single-column-600 .onboarding-model-group {
|
||||
@media (max-width: 760px) {
|
||||
.generation-technical-fields,
|
||||
.generation-runtime-fields {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
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%;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -634,6 +634,12 @@
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
@media (min-width: 761px) {
|
||||
.onboarding-r6-mode {
|
||||
min-height: 100%;
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
.onboarding-r6-empty {
|
||||
margin: 0;
|
||||
padding: 28px 18px;
|
||||
|
||||
@@ -271,6 +271,87 @@ it("reads Core's SDK context directly and passes native fields unchanged", async
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["omitted", raw],
|
||||
["null", { ...raw, turn_id: null, turn_index: null, turn_count: null }],
|
||||
[
|
||||
"minimum populated",
|
||||
{
|
||||
...raw,
|
||||
turn_id: "t",
|
||||
turn_index: 0,
|
||||
turn_count: 1,
|
||||
},
|
||||
],
|
||||
[
|
||||
"maximum populated",
|
||||
{
|
||||
...raw,
|
||||
turn_id: "t".repeat(96),
|
||||
turn_index: Number.MAX_SAFE_INTEGER,
|
||||
turn_count: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
],
|
||||
])("passes through %s optional raw turn metadata", async (_name, item) => {
|
||||
const f = await fixture();
|
||||
const response = receiptFor([item]);
|
||||
f.respond(response);
|
||||
|
||||
const result = await f.post();
|
||||
|
||||
expect(result.status).toBe(200);
|
||||
expect(result.data).toEqual(response);
|
||||
expect(f.diagnostics).not.toContainEqual(
|
||||
expect.objectContaining({ event: "context.response_invalid" }),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["empty turn_id", { turn_id: "" }],
|
||||
["overlong turn_id", { turn_id: "t".repeat(97) }],
|
||||
["non-string turn_id", { turn_id: 1 }],
|
||||
["negative turn_index", { turn_index: -1 }],
|
||||
["fractional turn_index", { turn_index: 0.5 }],
|
||||
["non-number turn_index", { turn_index: "0" }],
|
||||
["unsafe turn_index", { turn_index: 2 ** 53 }],
|
||||
["zero turn_count", { turn_count: 0 }],
|
||||
["negative turn_count", { turn_count: -1 }],
|
||||
["fractional turn_count", { turn_count: 1.5 }],
|
||||
["non-number turn_count", { turn_count: "1" }],
|
||||
["unsafe turn_count", { turn_count: 2 ** 53 }],
|
||||
["unknown field", { revision: 1 }],
|
||||
])("rejects raw context with %s", async (_name, fields) => {
|
||||
const f = await fixture();
|
||||
f.respond(receiptFor([{ ...raw, content: "private turn", ...fields }]));
|
||||
|
||||
const result = await f.post();
|
||||
|
||||
expect(result.status).toBe(502);
|
||||
expect(result.data).toEqual({ error: "context_unavailable" });
|
||||
expect(f.diagnostics).toContainEqual(
|
||||
expect.objectContaining({
|
||||
event: "context.response_invalid",
|
||||
error_code: "invalid_context_response",
|
||||
details: { reason: "response" },
|
||||
}),
|
||||
);
|
||||
expect(JSON.stringify(f.diagnostics)).not.toContain("private turn");
|
||||
});
|
||||
|
||||
it("rejects raw turn metadata on a memory context item", async () => {
|
||||
const f = await fixture();
|
||||
f.respond({
|
||||
...mixed,
|
||||
items: [{ ...memory, turn_id: "t" }],
|
||||
used_characters: [...memory.content].length,
|
||||
});
|
||||
|
||||
const result = await f.post();
|
||||
|
||||
expect(result.status).toBe(502);
|
||||
expect(result.data).toEqual({ error: "context_unavailable" });
|
||||
});
|
||||
|
||||
it.each([
|
||||
[10, 200],
|
||||
[11, 502],
|
||||
|
||||
@@ -184,17 +184,30 @@ function validSdkItem(value: unknown): boolean {
|
||||
if (!record(value) || typeof value.truncated !== "boolean") return false;
|
||||
if (value.kind === "raw")
|
||||
return (
|
||||
exact(value, [
|
||||
"kind",
|
||||
"raw_id",
|
||||
"content",
|
||||
"source",
|
||||
"session_id",
|
||||
"memory_at",
|
||||
"score",
|
||||
"truncated",
|
||||
]) &&
|
||||
exact(
|
||||
value,
|
||||
[
|
||||
"kind",
|
||||
"raw_id",
|
||||
"content",
|
||||
"source",
|
||||
"session_id",
|
||||
"memory_at",
|
||||
"score",
|
||||
"truncated",
|
||||
],
|
||||
["turn_id", "turn_index", "turn_count"],
|
||||
) &&
|
||||
text(value.raw_id, 1, 512) &&
|
||||
(value.turn_id === undefined ||
|
||||
value.turn_id === null ||
|
||||
text(value.turn_id, 1, 96)) &&
|
||||
(value.turn_index === undefined ||
|
||||
value.turn_index === null ||
|
||||
integer(value.turn_index, 0, Number.MAX_SAFE_INTEGER)) &&
|
||||
(value.turn_count === undefined ||
|
||||
value.turn_count === null ||
|
||||
integer(value.turn_count, 1, Number.MAX_SAFE_INTEGER)) &&
|
||||
text(value.content, 0, 20000) &&
|
||||
text(value.source, 1, 128) &&
|
||||
text(value.session_id, 1, 512) &&
|
||||
|
||||
Reference in new issue
Block a user