feat(agents): unify settings dialog for users and admins (#22914)

## Summary

Refactors the admin-only "Configure Agents" dialog into a unified
**Settings** dialog accessible to all users via a gear icon in the
sidebar.

### What changed

- **Settings gear in sidebar**: A gear icon now appears in the
bottom-left of the sidebar (next to the user avatar dropdown). Clicking
it opens the Settings dialog. This replaces the admin-only "Admin"
button that was in the top toolbar.

- **Custom Prompt tab** (all users): A new "Custom Prompt" tab is always
visible in the dialog. Users can write personal instructions that are
applied to all their new chats (stored per-user via the
`/api/experimental/chats/config/user-prompt` endpoint).

- **Admin tabs remain gated**: The Providers, Models, and Behavior
(system prompt) tabs only appear for admin users, preserving the
existing RBAC model.

- **API + query hooks**: Added `getUserChatCustomPrompt` /
`updateUserChatCustomPrompt` methods to the TypeScript API client and
corresponding React Query hooks.

### Files changed

| File | Change |
|------|--------|
| `site/src/api/api.ts` | Added GET/PUT methods for user custom prompt |
| `site/src/api/queries/chats.ts` | Added query/mutation hooks for user
custom prompt |
| `site/src/pages/AgentsPage/ConfigureAgentsDialog.tsx` | Added "Custom
Prompt" tab, renamed to "Settings" |
| `site/src/pages/AgentsPage/AgentsSidebar.tsx` | Added settings gear
button next to user dropdown |
| `site/src/pages/AgentsPage/AgentsPageView.tsx` | Removed "Admin"
button, pass `onOpenSettings` to sidebar |
| `site/src/pages/AgentsPage/AgentsPage.tsx` | Wired up user prompt
state, removed admin-only guard on dialog |
| `*.stories.tsx` | Updated to match new prop interfaces |
This commit is contained in:
Kyle Carberry
2026-03-10 12:52:54 -07:00
committed by GitHub
parent bfc58c8238
commit e7f8dfbe15
9 changed files with 358 additions and 142 deletions
+20
View File
@@ -3070,6 +3070,26 @@ class ApiMethods {
await this.axios.put("/api/experimental/chats/config/system-prompt", req);
};
getUserChatCustomPrompt =
async (): Promise<TypesGen.UserChatCustomPromptResponse> => {
const response =
await this.axios.get<TypesGen.UserChatCustomPromptResponse>(
"/api/experimental/chats/config/user-prompt",
);
return response.data;
};
updateUserChatCustomPrompt = async (
req: TypesGen.UpdateUserChatCustomPromptRequest,
): Promise<TypesGen.UserChatCustomPromptResponse> => {
const response =
await this.axios.put<TypesGen.UserChatCustomPromptResponse>(
"/api/experimental/chats/config/user-prompt",
req,
);
return response.data;
};
getChatProviderConfigs = async (): Promise<TypesGen.ChatProviderConfig[]> => {
const response = await this.axios.get<TypesGen.ChatProviderConfig[]>(
chatProviderConfigsPath,
+16
View File
@@ -270,6 +270,22 @@ export const updateChatSystemPrompt = (queryClient: QueryClient) => ({
},
});
const chatUserCustomPromptKey = ["chat-user-custom-prompt"] as const;
export const chatUserCustomPrompt = () => ({
queryKey: chatUserCustomPromptKey,
queryFn: () => API.getUserChatCustomPrompt(),
});
export const updateUserChatCustomPrompt = (queryClient: QueryClient) => ({
mutationFn: API.updateUserChatCustomPrompt,
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: chatUserCustomPromptKey,
});
},
});
export const chatModelsKey = ["chat-models"] as const;
export const chatModels = () => ({
@@ -51,6 +51,12 @@ const meta: Meta<typeof AgentCreateForm> = {
system_prompt: "",
});
spyOn(API, "updateChatSystemPrompt").mockResolvedValue();
spyOn(API, "getUserChatCustomPrompt").mockResolvedValue({
custom_prompt: "",
});
spyOn(API, "updateUserChatCustomPrompt").mockResolvedValue({
custom_prompt: "",
});
},
};
@@ -180,12 +186,20 @@ export const SavesBehaviorPromptAndRestores: Story = {
},
play: async () => {
const dialog = await screen.findByRole("dialog");
const textarea = await within(dialog).findByPlaceholderText(
"Optional. Set deployment-wide instructions for all new chats.",
// Find the System Instructions textarea by its unique placeholder.
const textareas = await within(dialog).findAllByPlaceholderText(
"Additional behavior, style, and tone preferences for all users",
);
const textarea = textareas[0];
await userEvent.type(textarea, "You are a focused coding assistant.");
await userEvent.click(within(dialog).getByRole("button", { name: "Save" }));
// Click the Save button inside the System Instructions form.
// There are multiple Save buttons (one per form), so grab all and
// pick the last one which belongs to the system prompt section.
const saveButtons = within(dialog).getAllByRole("button", { name: "Save" });
await userEvent.click(saveButtons[saveButtons.length - 1]);
await waitFor(() => {
expect(API.updateChatSystemPrompt).toHaveBeenCalledWith({
+48 -16
View File
@@ -1,4 +1,9 @@
import { chatSystemPrompt, updateChatSystemPrompt } from "api/queries/chats";
import {
chatSystemPrompt,
chatUserCustomPrompt,
updateChatSystemPrompt,
updateUserChatCustomPrompt,
} from "api/queries/chats";
import { workspaces } from "api/queries/workspaces";
import type * as TypesGen from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
@@ -144,6 +149,12 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
isPending: isSavingSystemPrompt,
isError: isSaveSystemPromptError,
} = useMutation(updateChatSystemPrompt(queryClient));
const userPromptQuery = useQuery(chatUserCustomPrompt());
const {
mutate: saveUserPrompt,
isPending: isSavingUserPrompt,
isError: isSaveUserPromptError,
} = useMutation(updateUserChatCustomPrompt(queryClient));
const [initialLastModelConfigID] = useState(() => {
if (typeof window === "undefined") {
return "";
@@ -206,6 +217,9 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
const serverPrompt = systemPromptQuery.data?.system_prompt ?? "";
const [localEdit, setLocalEdit] = useState<string | null>(null);
const systemPromptDraft = localEdit ?? serverPrompt;
const serverUserPrompt = userPromptQuery.data?.custom_prompt ?? "";
const [localUserEdit, setLocalUserEdit] = useState<string | null>(null);
const userPromptDraft = localUserEdit ?? serverUserPrompt;
const workspacesQuery = useQuery(workspaces({ q: "owner:me", limit: 0 }));
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string | null>(
() => {
@@ -215,7 +229,6 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
);
const workspaceOptions = workspacesQuery.data?.workspaces ?? [];
const autoCreateWorkspaceValue = "__auto_create_workspace__";
const hasAdminControls = canSetSystemPrompt || canManageChatModelConfigs;
const hasModelOptions = modelOptions.length > 0;
const hasConfiguredModels = hasConfiguredModelsInCatalog(modelCatalog);
const modelSelectorPlaceholder = getModelSelectorPlaceholder(
@@ -264,6 +277,22 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
const selectedModelRef = useRef(selectedModel);
selectedModelRef.current = selectedModel;
const isSystemPromptDirty = localEdit !== null && localEdit !== serverPrompt;
const isUserPromptDirty =
localUserEdit !== null && localUserEdit !== serverUserPrompt;
const handleSaveUserPrompt = useCallback(
(event: FormEvent) => {
event.preventDefault();
if (!isUserPromptDirty) {
return;
}
saveUserPrompt(
{ custom_prompt: userPromptDraft },
{ onSuccess: () => setLocalUserEdit(null) },
);
},
[isUserPromptDirty, userPromptDraft, saveUserPrompt],
);
const handleWorkspaceChange = (value: string) => {
if (value === autoCreateWorkspaceValue) {
@@ -432,20 +461,23 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
/>
</div>
{hasAdminControls && (
<ConfigureAgentsDialog
open={isConfigureAgentsDialogOpen}
onOpenChange={onConfigureAgentsDialogOpenChange}
canManageChatModelConfigs={canManageChatModelConfigs}
canSetSystemPrompt={canSetSystemPrompt}
systemPromptDraft={systemPromptDraft}
onSystemPromptDraftChange={setLocalEdit}
onSaveSystemPrompt={handleSaveSystemPrompt}
isSystemPromptDirty={isSystemPromptDirty}
saveSystemPromptError={isSaveSystemPromptError}
isDisabled={isCreating || isSavingSystemPrompt}
/>
)}
<ConfigureAgentsDialog
open={isConfigureAgentsDialogOpen}
onOpenChange={onConfigureAgentsDialogOpenChange}
canManageChatModelConfigs={canManageChatModelConfigs}
canSetSystemPrompt={canSetSystemPrompt}
systemPromptDraft={systemPromptDraft}
onSystemPromptDraftChange={setLocalEdit}
onSaveSystemPrompt={handleSaveSystemPrompt}
isSystemPromptDirty={isSystemPromptDirty}
saveSystemPromptError={isSaveSystemPromptError}
userPromptDraft={userPromptDraft}
onUserPromptDraftChange={setLocalUserEdit}
onSaveUserPrompt={handleSaveUserPrompt}
isUserPromptDirty={isUserPromptDirty}
saveUserPromptError={isSaveUserPromptError}
isDisabled={isCreating || isSavingSystemPrompt || isSavingUserPrompt}
/>
</div>
);
};
+1 -10
View File
@@ -123,6 +123,7 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
hasNextPage={hasNextPage}
onLoadMore={onLoadMore}
onCollapse={onCollapseSidebar}
onOpenSettings={() => setConfigureAgentsDialogOpen(true)}
/>
</div>
@@ -162,16 +163,6 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
<div className="flex items-center gap-2">
<ChimeButton />
<WebPushButton />
{isAgentsAdmin && (
<Button
variant="subtle"
disabled={isCreating}
className="h-8 gap-1.5 border-none bg-transparent px-1 text-[13px] shadow-none hover:bg-transparent"
onClick={() => setConfigureAgentsDialogOpen(true)}
>
Admin
</Button>
)}
</div>
</div>
<AgentCreateForm
+41 -26
View File
@@ -36,6 +36,7 @@ import {
Loader2Icon,
PanelLeftCloseIcon,
PauseIcon,
SettingsIcon,
SquarePenIcon,
Trash2Icon,
} from "lucide-react";
@@ -75,6 +76,7 @@ interface AgentsSidebarProps {
hasNextPage?: boolean;
onLoadMore?: () => void;
onCollapse?: () => void;
onOpenSettings?: () => void;
}
const statusConfig = {
@@ -542,6 +544,7 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
hasNextPage,
onLoadMore,
onCollapse,
onOpenSettings,
} = props;
const { agentId, chatId } = useParams<{
agentId?: string;
@@ -814,36 +817,48 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
</div>
</ScrollArea>
<div className="hidden border-0 border-t border-solid md:block">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="flex items-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex min-w-0 flex-1 items-center gap-2 bg-transparent border-0 cursor-pointer px-3 py-3 text-left hover:bg-surface-tertiary/50 transition-colors"
>
<Avatar
fallback={user.username}
src={user.avatar_url}
size="sm"
className="rounded-full"
/>{" "}
<span className="truncate text-sm text-content-secondary">
{user.name || user.username}
</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="min-w-auto w-[260px]">
<UserDropdownContent
user={user}
buildInfo={buildInfo}
supportLinks={
appearance.support_links?.filter(
(link) => link.location !== "navbar",
) ?? []
}
onSignOut={signOut}
/>
</DropdownMenuContent>
</DropdownMenu>
{onOpenSettings && (
<button
type="button"
className="flex w-full items-center gap-2 bg-transparent border-0 cursor-pointer px-3 py-3 text-left hover:bg-surface-tertiary/50 transition-colors"
onClick={onOpenSettings}
className="flex shrink-0 items-center justify-center bg-transparent border-0 cursor-pointer p-2 mr-1 rounded-md text-content-secondary hover:text-content-primary hover:bg-surface-tertiary/50 transition-colors"
aria-label="Settings"
>
<Avatar
fallback={user.username}
src={user.avatar_url}
size="sm"
className="rounded-full"
/>{" "}
<span className="truncate text-sm text-content-secondary">
{user.name || user.username}
</span>
<SettingsIcon className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="min-w-auto w-[260px]">
<UserDropdownContent
user={user}
buildInfo={buildInfo}
supportLinks={
appearance.support_links?.filter(
(link) => link.location !== "navbar",
) ?? []
}
onSignOut={signOut}
/>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
</div>
);
@@ -79,6 +79,11 @@ const meta: Meta<typeof ConfigureAgentsDialog> = {
onSaveSystemPrompt: fn(),
isSystemPromptDirty: false,
saveSystemPromptError: false,
userPromptDraft: "",
onUserPromptDraftChange: fn(),
onSaveUserPrompt: fn(),
isUserPromptDirty: false,
saveUserPromptError: false,
isDisabled: false,
},
};
@@ -86,23 +91,19 @@ const meta: Meta<typeof ConfigureAgentsDialog> = {
export default meta;
type Story = StoryObj<typeof ConfigureAgentsDialog>;
export const SystemPromptOnly: Story = {
/** Regular user sees only the Personal Prompt section. */
export const UserOnly: Story = {};
/** Admin sees Personal Prompt + System Prompt in the same Prompts tab. */
export const AdminPrompts: Story = {
args: {
canSetSystemPrompt: true,
canManageChatModelConfigs: false,
systemPromptDraft: "You are a helpful coding assistant.",
},
};
export const ModelConfigOnly: Story = {
args: {
canSetSystemPrompt: false,
canManageChatModelConfigs: true,
},
parameters: { queries: chatQueries },
};
export const BothEnabled: Story = {
/** Admin with model config permissions sees Providers/Models tabs. */
export const AdminFull: Story = {
args: {
canSetSystemPrompt: true,
canManageChatModelConfigs: true,
@@ -7,22 +7,54 @@ import {
DialogHeader,
DialogTitle,
} from "components/Dialog/Dialog";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import type { LucideIcon } from "lucide-react";
import { BoxesIcon, KeyRoundIcon, UserIcon, XIcon } from "lucide-react";
import {
BoxesIcon,
KeyRoundIcon,
ShieldIcon,
UserIcon,
XIcon,
} from "lucide-react";
import { type FC, type FormEvent, useEffect, useMemo, useState } from "react";
import TextareaAutosize from "react-textarea-autosize";
import { cn } from "utils/cn";
import { ChatModelAdminPanel } from "./ChatModelAdminPanel/ChatModelAdminPanel";
import { SectionHeader } from "./SectionHeader";
type ConfigureAgentsSection = "providers" | "system-prompt" | "models";
type ConfigureAgentsSection = "providers" | "models" | "behavior";
type ConfigureAgentsSectionOption = {
id: ConfigureAgentsSection;
label: string;
icon: LucideIcon;
adminOnly?: boolean;
};
const AdminBadge: FC = () => (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex cursor-default items-center gap-1 rounded bg-surface-tertiary/60 px-1.5 py-px text-[11px] font-medium text-content-secondary">
<ShieldIcon className="h-3 w-3" />
Admin
</span>
</TooltipTrigger>
<TooltipContent side="right">
Only visible to deployment administrators.
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
const textareaClassName =
"max-h-[240px] w-full resize-none overflow-y-auto rounded-lg border border-border bg-surface-primary px-4 py-3 font-sans text-[13px] leading-relaxed text-content-primary placeholder:text-content-secondary focus:outline-none focus:ring-2 focus:ring-content-link/30 [scrollbar-width:thin]";
interface ConfigureAgentsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
@@ -33,6 +65,11 @@ interface ConfigureAgentsDialogProps {
onSaveSystemPrompt: (event: FormEvent) => void;
isSystemPromptDirty: boolean;
saveSystemPromptError: boolean;
userPromptDraft: string;
onUserPromptDraftChange: (value: string) => void;
onSaveUserPrompt: (event: FormEvent) => void;
isUserPromptDirty: boolean;
saveUserPromptError: boolean;
isDisabled: boolean;
}
@@ -46,64 +83,64 @@ export const ConfigureAgentsDialog: FC<ConfigureAgentsDialogProps> = ({
onSaveSystemPrompt,
isSystemPromptDirty,
saveSystemPromptError,
userPromptDraft,
onUserPromptDraftChange,
onSaveUserPrompt,
isUserPromptDirty,
saveUserPromptError,
isDisabled,
}) => {
const configureSectionOptions = useMemo<
readonly ConfigureAgentsSectionOption[]
>(() => {
const options: ConfigureAgentsSectionOption[] = [];
options.push({
id: "behavior",
label: "Behavior",
icon: UserIcon,
});
if (canManageChatModelConfigs) {
options.push({
id: "providers",
label: "Providers",
icon: KeyRoundIcon,
adminOnly: true,
});
options.push({
id: "models",
label: "Models",
icon: BoxesIcon,
});
}
if (canSetSystemPrompt) {
options.push({
id: "system-prompt",
label: "Behavior",
icon: UserIcon,
adminOnly: true,
});
}
return options;
}, [canManageChatModelConfigs, canSetSystemPrompt]);
}, [canManageChatModelConfigs]);
const [userActiveSection, setUserActiveSection] =
useState<ConfigureAgentsSection>("providers");
useState<ConfigureAgentsSection>("behavior");
// Derive the effective section — validated against current options
// every render so we never show an unavailable tab.
const activeSection = configureSectionOptions.some(
(s) => s.id === userActiveSection,
)
? userActiveSection
: (configureSectionOptions[0]?.id ?? "providers");
: (configureSectionOptions[0]?.id ?? "behavior");
// Reset to the preferred initial section each time the dialog opens.
useEffect(() => {
if (open) {
setUserActiveSection("providers");
setUserActiveSection("behavior");
}
}, [open]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="grid h-[min(88dvh,720px)] max-w-4xl grid-cols-1 gap-0 overflow-hidden p-0 md:grid-cols-[220px_minmax(0,1fr)]">
{/* Visually hidden for accessibility */}
<DialogHeader className="sr-only">
<DialogTitle>Configure Agents</DialogTitle>
<DialogTitle>Settings</DialogTitle>
<DialogDescription>
Manage providers, system prompt, and available models.
Manage your personal preferences and agent configuration.
</DialogDescription>
</DialogHeader>
{/* Sidebar */}
<nav className="flex flex-row gap-0.5 overflow-x-auto border-b border-border bg-surface-secondary/40 p-2 md:flex-col md:gap-0.5 md:overflow-x-visible md:border-b-0 md:border-r md:p-4">
<DialogClose asChild>
<Button
@@ -131,71 +168,154 @@ export const ConfigureAgentsDialog: FC<ConfigureAgentsDialogProps> = ({
onClick={() => setUserActiveSection(section.id)}
>
<SectionIcon className="h-5 w-5 shrink-0" />
<span className="text-sm font-medium">{section.label}</span>
<span className="flex items-center gap-2 text-sm font-medium">
{section.label}
{section.adminOnly && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex">
<ShieldIcon className="h-3 w-3 shrink-0 opacity-50" />
</span>
</TooltipTrigger>
<TooltipContent side="right">Admin only</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</span>
</Button>
);
})}
</nav>
{/* Content */}
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto px-6 py-5">
{activeSection === "providers" && canManageChatModelConfigs && (
<ChatModelAdminPanel section="providers" sectionLabel="Providers" />
)}
{activeSection === "system-prompt" && canSetSystemPrompt && (
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto px-6 py-5 [scrollbar-width:thin] [scrollbar-color:hsl(var(--surface-quaternary))_transparent]">
{activeSection === "behavior" && (
<>
<SectionHeader label="Behavior" />
<SectionHeader
label="Behavior"
description="Custom instructions that shape how the agent responds in your chats."
/>
{/* ── Personal prompt (always visible) ── */}
<form
className="space-y-4"
onSubmit={(event) => void onSaveSystemPrompt(event)}
className="space-y-2"
onSubmit={(event) => void onSaveUserPrompt(event)}
>
<div className="space-y-2">
<h3 className="m-0 text-[13px] font-semibold text-content-primary">
System Prompt
</h3>
<p className="m-0 text-xs text-content-secondary">
Admin-only instruction applied to all new chats. When empty,
the built-in default prompt is used.
</p>
<TextareaAutosize
className="min-h-[220px] w-full resize-y rounded-lg border border-border bg-surface-primary px-4 py-3 font-sans text-[13px] leading-relaxed text-content-primary placeholder:text-content-secondary focus:outline-none focus:ring-2 focus:ring-content-link/30"
placeholder="Optional. Set deployment-wide instructions for all new chats."
value={systemPromptDraft}
onChange={(event) =>
onSystemPromptDraftChange(event.target.value)
}
disabled={isDisabled}
minRows={7}
/>
<div className="flex justify-end gap-2">
<Button
size="sm"
variant="outline"
type="button"
onClick={() => onSystemPromptDraftChange("")}
disabled={isDisabled || !systemPromptDraft}
>
Clear
</Button>
<Button
size="sm"
type="submit"
disabled={isDisabled || !isSystemPromptDirty}
>
Save
</Button>
</div>
{saveSystemPromptError && (
<p className="m-0 text-xs text-content-destructive">
Failed to save system prompt.
</p>
)}
<h3 className="m-0 text-[13px] font-semibold text-content-primary">
Personal Instructions
</h3>
<p className="!mt-0.5 m-0 text-xs text-content-secondary">
Applied to all your chats. Only visible to you.
</p>{" "}
<TextareaAutosize
className={textareaClassName}
placeholder="Additional behavior, style, and tone preferences"
value={userPromptDraft}
onChange={(event) =>
onUserPromptDraftChange(event.target.value)
}
disabled={isDisabled}
minRows={1}
/>
<div className="flex justify-end gap-2">
<Button
size="sm"
variant="outline"
type="button"
onClick={() => onUserPromptDraftChange("")}
disabled={isDisabled || !userPromptDraft}
>
Clear
</Button>
<Button
size="sm"
type="submit"
disabled={isDisabled || !isUserPromptDirty}
>
Save
</Button>
</div>
{saveUserPromptError && (
<p className="m-0 text-xs text-content-destructive">
Failed to save personal instructions.
</p>
)}
</form>
{/* ── Admin system prompt (admin only) ── */}
{canSetSystemPrompt && (
<>
<hr className="my-5 border-0 border-t border-solid border-border" />
<form
className="space-y-2"
onSubmit={(event) => void onSaveSystemPrompt(event)}
>
<div className="flex items-center gap-2">
<h3 className="m-0 text-[13px] font-semibold text-content-primary">
System Instructions
</h3>
<AdminBadge />
</div>
<p className="!mt-0.5 m-0 text-xs text-content-secondary">
Applied to all chats for every user. When empty, the
built-in default is used.
</p>{" "}
<TextareaAutosize
className={textareaClassName}
placeholder="Additional behavior, style, and tone preferences for all users"
value={systemPromptDraft}
onChange={(event) =>
onSystemPromptDraftChange(event.target.value)
}
disabled={isDisabled}
minRows={1}
/>
<div className="flex justify-end gap-2">
<Button
size="sm"
variant="outline"
type="button"
onClick={() => onSystemPromptDraftChange("")}
disabled={isDisabled || !systemPromptDraft}
>
Clear
</Button>
<Button
size="sm"
type="submit"
disabled={isDisabled || !isSystemPromptDirty}
>
Save
</Button>
</div>
{saveSystemPromptError && (
<p className="m-0 text-xs text-content-destructive">
Failed to save system prompt.
</p>
)}
</form>
</>
)}
</>
)}
{activeSection === "providers" && canManageChatModelConfigs && (
<>
<SectionHeader
label="Providers"
description="Connect third-party LLM services like OpenAI, Anthropic, or Google. Each provider supplies models that users can select for their chats."
badge={<AdminBadge />}
/>{" "}
<ChatModelAdminPanel section="providers" />
</>
)}
{activeSection === "models" && canManageChatModelConfigs && (
<ChatModelAdminPanel section="models" sectionLabel="Models" />
<>
<SectionHeader
label="Models"
description="Choose which models from your configured providers are available for users to select. You can set a default and adjust context limits."
badge={<AdminBadge />}
/>{" "}
<ChatModelAdminPanel section="models" />
</>
)}
</div>
</DialogContent>
+11 -4
View File
@@ -3,22 +3,29 @@ import type { FC, ReactNode } from "react";
interface SectionHeaderProps {
label: string;
description?: string;
badge?: ReactNode;
action?: ReactNode;
}
export const SectionHeader: FC<SectionHeaderProps> = ({
label,
description,
badge,
action,
}) => (
<>
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="m-0 text-lg font-medium text-content-primary">
{label}
</h2>
<div className="flex items-center gap-2">
<h2 className="m-0 text-lg font-medium text-content-primary">
{label}
</h2>
{badge}
</div>
{description && (
<p className="m-0 text-sm text-content-secondary">{description}</p>
<p className="m-0 mt-0.5 text-sm text-content-secondary">
{description}
</p>
)}
</div>
{action}