Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const english = {
"例如 deepseek/deepseek-v3": "For example, deepseek/deepseek-v3",
"例如 siliconflow": "For example, siliconflow",
"例如 team-ppio": "For example, team-ppio",
"留空则使用 Profile ID": "Leave blank to use the Profile ID",
"例如 团队 PPIO": "For example, Team PPIO",
"粘贴你的 API Key": "Paste your API key",
"隐藏密钥": "Hide API key",
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/pages/ProfilesPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,34 @@ describe("ProfilesPage", () => {
})));
});

it("saves a Profile whose name was left blank", async () => {
// The backend fills an empty label in from the existing value or the ID
// (internal/profile/write.go:71-74), so requiring one here was stricter than
// the write path and blocked a rename-to-nothing edit.
const save = vi.spyOn(api, "saveProfile").mockResolvedValue(profile());
renderPage([profile()]);
fireEvent.click(screen.getByRole("button", { name: "编辑 团队 PPIO" }));
const label = screen.getByLabelText("名称");
expect(label.hasAttribute("required")).toBe(false);
fireEvent.change(label, { target: { value: "" } });

const button = screen.getByRole("button", { name: "保存 Profile" });
expect(button.hasAttribute("disabled")).toBe(false);
fireEvent.click(button);
await waitFor(() => expect(save).toHaveBeenCalledWith(expect.objectContaining({ id: "team-ppio", label: "" })));
});

it("still refuses to save without a model", async () => {
// model has no backend fallback (write.go:50-53), so this one stays required.
const save = vi.spyOn(api, "saveProfile");
renderPage([profile()]);
fireEvent.click(screen.getByRole("button", { name: "编辑 团队 PPIO" }));
fireEvent.change(screen.getByLabelText("模型"), { target: { value: "" } });

expect(screen.getByRole("button", { name: "保存 Profile" }).hasAttribute("disabled")).toBe(true);
expect(save).not.toHaveBeenCalled();
});

it("applies one Profile to all of its Agents", async () => {
const install = vi.spyOn(api, "install").mockResolvedValue({
ok: true,
Expand Down
19 changes: 15 additions & 4 deletions frontend/src/pages/ProfilesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ export function ProfilesPage() {
const nameOf = (agentId: string) =>
status.catalog.find((item) => item.id === agentId)?.name || agentId;
const providerHasKey = Boolean(editor && status.providers[editor.provider]?.has_key);
const canSave = Boolean(
editor?.id.trim() && editor.label.trim() && editor.model.trim() && editor.agentIds.length,
);
// label is absent on purpose: the backend fills it in from the existing value
// or the ID, so demanding it here was stricter than the write path.
const canSave = Boolean(editor?.id.trim() && editor.model.trim() && editor.agentIds.length);

// Creating a Profile goes through onboarding: it collects the Agent, key,
// model and name in order, tests the connection, and the install writes the
Expand Down Expand Up @@ -163,7 +163,18 @@ export function ProfilesPage() {
</div>
<div className="field-stack">
<label htmlFor="profile-label">{t("名称")}</label>
<input id="profile-label" value={editor.label} onChange={(event) => setEditor({ ...editor, label: event.target.value })} placeholder={t("例如 团队 PPIO")} required />
{/* Optional, matching the backend: an empty label falls back to the
existing one, then to the ID (internal/profile/write.go:71-74).
The hint says so, or a Profile saved without a name looks like it
lost it. */}
<input
id="profile-label"
value={editor.label}
onChange={(event) => setEditor({ ...editor, label: event.target.value })}
placeholder={t("例如 团队 PPIO")}
aria-describedby="profile-label-hint"
/>
<small id="profile-label-hint">{t("留空则使用 Profile ID")}</small>
</div>
<div className="profile-editor-wide">
<ProviderSegment
Expand Down