mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user