Files
zn-ai/src/pages/Agents/components/AgentSettingsDialog.tsx
duanshuwen 3de3629d12 feat: add AgentsConfirmDialog component and integrate it into AgentsPage for agent deletion confirmation
- Implemented AgentsConfirmDialog for confirming agent deletions.
- Updated AgentsPage to manage agent deletion state and feedback messages.
- Refactored provider account loading logic to include channel groups.
- Enhanced feedback mechanism for user actions such as agent creation and deletion.
2026-04-19 22:10:01 +08:00

712 lines
27 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { RefreshCw, X } from 'lucide-react';
import type { AgentSummary } from '@runtime/lib/agents';
import type { ProviderAccount } from '@runtime/lib/providers';
import type { ChannelAccountCatalogGroup } from '../../../lib/channel-types';
import type { ProviderVendorInfo, ProviderWithKeyInfo } from '../../../lib/providers';
import telegramIcon from '../../../assets/channels/telegram.svg';
import discordIcon from '../../../assets/channels/discord.svg';
import whatsappIcon from '../../../assets/channels/whatsapp.svg';
import wechatIcon from '../../../assets/channels/wechat.svg';
import dingtalkIcon from '../../../assets/channels/dingtalk.svg';
import feishuIcon from '../../../assets/channels/feishu.svg';
import wecomIcon from '../../../assets/channels/wecom.svg';
import qqIcon from '../../../assets/channels/qq.svg';
import AgentsConfirmDialog from './AgentsConfirmDialog';
const inputClasses = [
'h-[44px] flex-1 rounded-xl border border-black/10 bg-[#F8F4EC] px-4 text-[14px] text-[#171717]',
'outline-none transition-colors placeholder:text-[#99A0AE] focus:border-black/20',
'disabled:cursor-not-allowed disabled:opacity-65 dark:border-white/10 dark:bg-[#222225] dark:text-[#f3f4f6] dark:placeholder:text-gray-500 dark:focus:border-white/20',
].join(' ');
const selectClasses = [
inputClasses,
'appearance-none pr-10',
].join(' ');
const labelClasses = 'text-[12px] text-[#525866] dark:text-gray-400';
type FeedbackTone = 'success' | 'error' | 'info';
type AgentSettingsDialogCopy = {
title: string;
description: string;
nameLabel: string;
saveLabel: string;
savingLabel: string;
closeLabel: string;
agentIdLabel: string;
modelLabel: string;
notConfigured: string;
inherited: string;
channelsTitle: string;
channelsDescription: string;
noChannels: string;
channelsManagedInChannels: string;
mainAccount: string;
cancelLabel: string;
unsavedChangesTitle: string;
unsavedChangesMessage: string;
closeWithoutSaving: string;
modelOverrideDescription: string;
modelProviderLabel: string;
modelProviderPlaceholder: string;
modelIdLabel: string;
modelIdPlaceholder: string;
modelPreview: string;
modelProviderEmpty: string;
useDefaultModel: string;
modelProviderRequired: string;
modelIdRequired: string;
modelInvalid: string;
nameSaved: string;
nameSaveFailedPrefix: string;
modelSaved: string;
modelReset: string;
modelSaveFailedPrefix: string;
};
type AgentSettingsDialogProps = {
open: boolean;
agent: AgentSummary | null;
channelGroups: ChannelAccountCatalogGroup[];
providerAccounts: ProviderAccount[];
providerStatuses: ProviderWithKeyInfo[];
providerVendors: ProviderVendorInfo[];
providerDefaultAccountId: string | null;
defaultModelRef: string | null;
copy: AgentSettingsDialogCopy;
onClose: () => void;
onUpdateName: (agentId: string, name: string) => Promise<void> | void;
onUpdateModel: (agentId: string, modelRef: string | null) => Promise<void> | void;
onFeedback?: (message: string, tone?: FeedbackTone) => void;
};
type RuntimeProviderOption = {
runtimeProviderKey: string;
accountId: string;
label: string;
modelIdPlaceholder?: string;
configuredModelId?: string;
};
function resolveRuntimeProviderKey(account: ProviderAccount): string {
if (account.authMode === 'oauth_browser') {
if (account.vendorId === 'google') return 'google-gemini-cli';
if (account.vendorId === 'openai') return 'openai-codex';
}
if (account.vendorId === 'custom' || account.vendorId === 'ollama') {
const suffix = account.id.replace(/-/g, '').slice(0, 8);
return `${account.vendorId}-${suffix}`;
}
if (account.vendorId === 'minimax-portal-cn') {
return 'minimax-portal';
}
return account.vendorId;
}
function splitModelRef(modelRef: string | null | undefined): { providerKey: string; modelId: string } | null {
const value = String(modelRef ?? '').trim();
if (!value) return null;
const separatorIndex = value.indexOf('/');
if (separatorIndex <= 0 || separatorIndex >= value.length - 1) return null;
return {
providerKey: value.slice(0, separatorIndex),
modelId: value.slice(separatorIndex + 1),
};
}
function hasConfiguredProviderCredentials(
account: ProviderAccount,
statusById: Map<string, ProviderWithKeyInfo>,
): boolean {
if (account.authMode === 'oauth_device' || account.authMode === 'oauth_browser' || account.authMode === 'local') {
return true;
}
return statusById.get(account.id)?.hasKey ?? false;
}
function getChannelDisplayName(group: ChannelAccountCatalogGroup): string {
return group.channelLabel || group.channelType;
}
function ChannelLogo({ type }: { type: string }) {
switch (type) {
case 'telegram':
return <img src={telegramIcon} alt="Telegram" className="h-5.5 w-5.5 dark:invert" />;
case 'discord':
return <img src={discordIcon} alt="Discord" className="h-5.5 w-5.5 dark:invert" />;
case 'whatsapp':
return <img src={whatsappIcon} alt="WhatsApp" className="h-5.5 w-5.5 dark:invert" />;
case 'wechat':
return <img src={wechatIcon} alt="WeChat" className="h-5.5 w-5.5 dark:invert" />;
case 'dingtalk':
return <img src={dingtalkIcon} alt="DingTalk" className="h-5.5 w-5.5 dark:invert" />;
case 'feishu':
return <img src={feishuIcon} alt="Feishu" className="h-5.5 w-5.5 dark:invert" />;
case 'wecom':
return <img src={wecomIcon} alt="WeCom" className="h-5.5 w-5.5 dark:invert" />;
case 'qqbot':
return <img src={qqIcon} alt="QQ Bot" className="h-5.5 w-5.5 dark:invert" />;
default:
return <span className="text-[22px] font-semibold">{type.slice(0, 1).toUpperCase() || 'C'}</span>;
}
}
function AgentModelDialog({
open,
agent,
providerAccounts,
providerStatuses,
providerVendors,
providerDefaultAccountId,
defaultModelRef,
copy,
onClose,
onUpdateModel,
onFeedback,
}: {
open: boolean;
agent: AgentSummary;
providerAccounts: ProviderAccount[];
providerStatuses: ProviderWithKeyInfo[];
providerVendors: ProviderVendorInfo[];
providerDefaultAccountId: string | null;
defaultModelRef: string | null;
copy: AgentSettingsDialogCopy;
onClose: () => void;
onUpdateModel: (agentId: string, modelRef: string | null) => Promise<void> | void;
onFeedback?: (message: string, tone?: FeedbackTone) => void;
}) {
const [selectedRuntimeProviderKey, setSelectedRuntimeProviderKey] = useState('');
const [modelIdInput, setModelIdInput] = useState('');
const [savingModel, setSavingModel] = useState(false);
const [showCloseConfirm, setShowCloseConfirm] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const runtimeProviderOptions = useMemo<RuntimeProviderOption[]>(() => {
const vendorMap = new Map<string, ProviderVendorInfo>(providerVendors.map((vendor) => [vendor.id, vendor]));
const statusById = new Map<string, ProviderWithKeyInfo>(providerStatuses.map((status) => [status.id, status]));
const entries = providerAccounts
.filter((account) => account.enabled && hasConfiguredProviderCredentials(account, statusById))
.sort((left, right) => {
if (left.id === providerDefaultAccountId) return -1;
if (right.id === providerDefaultAccountId) return 1;
return right.updatedAt.localeCompare(left.updatedAt);
});
const deduped = new Map<string, RuntimeProviderOption>();
for (const account of entries) {
const runtimeProviderKey = resolveRuntimeProviderKey(account);
if (!runtimeProviderKey || deduped.has(runtimeProviderKey)) continue;
const vendor = vendorMap.get(account.vendorId);
const configuredModelId = account.model
? (account.model.startsWith(`${runtimeProviderKey}/`)
? account.model.slice(runtimeProviderKey.length + 1)
: account.model)
: undefined;
deduped.set(runtimeProviderKey, {
runtimeProviderKey,
accountId: account.id,
label: `${account.label} (${vendor?.name || account.vendorId})`,
modelIdPlaceholder: vendor?.modelIdPlaceholder,
configuredModelId,
});
}
return [...deduped.values()];
}, [providerAccounts, providerDefaultAccountId, providerStatuses, providerVendors]);
useEffect(() => {
if (!open) return;
const override = splitModelRef(agent.overrideModelRef);
if (override) {
setSelectedRuntimeProviderKey(override.providerKey);
setModelIdInput(override.modelId);
setErrorMessage(null);
return;
}
const effective = splitModelRef(agent.modelRef || defaultModelRef);
if (effective) {
setSelectedRuntimeProviderKey(effective.providerKey);
setModelIdInput(effective.modelId);
setErrorMessage(null);
return;
}
setSelectedRuntimeProviderKey(runtimeProviderOptions[0]?.runtimeProviderKey || '');
setModelIdInput('');
setErrorMessage(null);
}, [agent.modelRef, agent.overrideModelRef, defaultModelRef, open, runtimeProviderOptions]);
if (!open) return null;
const selectedProvider = runtimeProviderOptions.find((option) => option.runtimeProviderKey === selectedRuntimeProviderKey) || null;
const trimmedModelId = modelIdInput.trim();
const nextModelRef = selectedRuntimeProviderKey && trimmedModelId
? `${selectedRuntimeProviderKey}/${trimmedModelId}`
: '';
const normalizedDefaultModelRef = String(defaultModelRef ?? '').trim();
const isUsingDefaultModelInForm = Boolean(normalizedDefaultModelRef) && nextModelRef === normalizedDefaultModelRef;
const currentOverrideModelRef = String(agent.overrideModelRef ?? '').trim();
const desiredOverrideModelRef = nextModelRef && nextModelRef !== normalizedDefaultModelRef
? nextModelRef
: null;
const modelChanged = (desiredOverrideModelRef || '') !== currentOverrideModelRef;
function handleRequestClose() {
if (savingModel || modelChanged) {
setShowCloseConfirm(true);
return;
}
onClose();
}
async function handleSaveModel(): Promise<void> {
if (!selectedRuntimeProviderKey) {
setErrorMessage(copy.modelProviderRequired);
return;
}
if (!trimmedModelId) {
setErrorMessage(copy.modelIdRequired);
return;
}
if (!modelChanged) return;
if (!nextModelRef.includes('/')) {
setErrorMessage(copy.modelInvalid);
return;
}
setSavingModel(true);
setErrorMessage(null);
try {
await onUpdateModel(agent.id, desiredOverrideModelRef);
onFeedback?.(desiredOverrideModelRef ? copy.modelSaved : copy.modelReset, 'success');
onClose();
} catch (error) {
setErrorMessage(`${copy.modelSaveFailedPrefix}${error instanceof Error ? error.message : String(error)}`);
} finally {
setSavingModel(false);
}
}
function handleUseDefaultModel() {
const parsedDefault = splitModelRef(normalizedDefaultModelRef);
if (!parsedDefault) {
setSelectedRuntimeProviderKey('');
setModelIdInput('');
setErrorMessage(null);
return;
}
setSelectedRuntimeProviderKey(parsedDefault.providerKey);
setModelIdInput(parsedDefault.modelId);
setErrorMessage(null);
}
return (
<>
<div className="fixed inset-0 z-60 flex items-center justify-center bg-black/50 p-4">
<div className="w-full max-w-xl overflow-hidden rounded-3xl bg-[#f3f1e9] shadow-2xl dark:bg-[#1f1f22]">
<div className="flex items-start justify-between gap-4 px-6 pb-2 pt-6">
<div>
<h3
className="text-[26px] font-normal leading-none tracking-tight text-[#171717] dark:text-[#f3f4f6]"
style={{ fontFamily: "Georgia, Cambria, 'Times New Roman', Times, serif" }}
>
{copy.modelLabel}
</h3>
<p className="mt-3 text-[15px] leading-6 text-[#525866] dark:text-gray-400">
{copy.modelOverrideDescription.replace('{defaultModel}', defaultModelRef || '-')}
</p>
</div>
<button
type="button"
onClick={handleRequestClose}
className="rounded-full p-1 text-[#99A0AE] transition-colors hover:text-[#171717] dark:hover:text-[#f3f4f6]"
aria-label={copy.closeLabel}
>
<X className="h-5 w-5" />
</button>
</div>
<div className="space-y-4 px-6 pb-6 pt-4">
{errorMessage ? (
<div className="rounded-[14px] border border-red-500/20 bg-red-500/10 px-4 py-3 text-[13px] text-red-600 dark:text-red-300">
{errorMessage}
</div>
) : null}
<div className="space-y-2">
<label htmlFor="agent-model-provider" className={labelClasses}>{copy.modelProviderLabel}</label>
<select
id="agent-model-provider"
value={selectedRuntimeProviderKey}
onChange={(event) => {
const nextProvider = event.target.value;
setSelectedRuntimeProviderKey(nextProvider);
setErrorMessage(null);
if (!modelIdInput.trim()) {
const option = runtimeProviderOptions.find((candidate) => candidate.runtimeProviderKey === nextProvider);
setModelIdInput(option?.configuredModelId || '');
}
}}
className={selectClasses}
>
<option value="">{copy.modelProviderPlaceholder}</option>
{runtimeProviderOptions.map((option) => (
<option key={option.runtimeProviderKey} value={option.runtimeProviderKey}>
{option.label}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label htmlFor="agent-model-id" className={labelClasses}>{copy.modelIdLabel}</label>
<input
id="agent-model-id"
value={modelIdInput}
onChange={(event) => {
setModelIdInput(event.target.value);
setErrorMessage(null);
}}
placeholder={selectedProvider?.modelIdPlaceholder || selectedProvider?.configuredModelId || copy.modelIdPlaceholder}
className={inputClasses}
/>
</div>
{nextModelRef ? (
<p className="break-all text-[12px] font-mono text-[#525866] dark:text-gray-400">
{copy.modelPreview}: {nextModelRef}
</p>
) : null}
{runtimeProviderOptions.length === 0 ? (
<p className="text-[12px] text-amber-600 dark:text-amber-400">
{copy.modelProviderEmpty}
</p>
) : null}
<div className="flex items-center justify-end gap-2 pt-2">
<button
type="button"
onClick={handleUseDefaultModel}
disabled={savingModel || !normalizedDefaultModelRef || isUsingDefaultModelInForm}
className="inline-flex h-9 items-center rounded-full border border-black/10 bg-transparent px-4 text-[13px] font-medium text-[#171717]/80 transition-colors hover:bg-black/5 hover:text-[#171717] disabled:cursor-not-allowed disabled:opacity-60 dark:border-white/10 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-[#f3f4f6]"
>
{copy.useDefaultModel}
</button>
<button
type="button"
onClick={handleRequestClose}
className="inline-flex h-9 items-center rounded-full border border-black/10 bg-transparent px-4 text-[13px] font-medium text-[#171717]/80 transition-colors hover:bg-black/5 hover:text-[#171717] disabled:cursor-not-allowed disabled:opacity-60 dark:border-white/10 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-[#f3f4f6]"
disabled={savingModel}
>
{copy.cancelLabel}
</button>
<button
type="button"
onClick={() => {
void handleSaveModel();
}}
disabled={savingModel || !selectedRuntimeProviderKey || !trimmedModelId || !modelChanged}
className="inline-flex h-9 items-center rounded-full bg-[#7E9DF5] px-4 text-[13px] font-medium text-white transition-colors hover:bg-[#6E90F3] disabled:cursor-not-allowed disabled:opacity-60"
>
{savingModel ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
copy.saveLabel
)}
</button>
</div>
</div>
</div>
</div>
<AgentsConfirmDialog
open={showCloseConfirm}
title={copy.unsavedChangesTitle}
message={copy.unsavedChangesMessage}
cancelLabel={copy.cancelLabel}
confirmLabel={copy.closeWithoutSaving}
onClose={() => setShowCloseConfirm(false)}
onConfirm={() => {
setShowCloseConfirm(false);
onClose();
}}
/>
</>
);
}
export default function AgentSettingsDialog({
open,
agent,
channelGroups,
providerAccounts,
providerStatuses,
providerVendors,
providerDefaultAccountId,
defaultModelRef,
copy,
onClose,
onUpdateName,
onUpdateModel,
onFeedback,
}: AgentSettingsDialogProps) {
const [name, setName] = useState('');
const [savingName, setSavingName] = useState(false);
const [showModelDialog, setShowModelDialog] = useState(false);
const [showCloseConfirm, setShowCloseConfirm] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
useEffect(() => {
if (!agent || !open) return;
setName(agent.name);
setErrorMessage(null);
}, [agent, open]);
const assignedChannels = useMemo(() => {
if (!agent) return [];
return channelGroups.flatMap((group) =>
group.accounts
.filter((account) => account.agentId === agent.id)
.map((account) => ({
channelType: group.channelType,
channelLabel: getChannelDisplayName(group),
accountId: account.accountId,
name: account.accountId === 'default' ? copy.mainAccount : account.name || account.accountId,
error: account.lastError,
})),
);
}, [agent, channelGroups, copy.mainAccount]);
useEffect(() => {
if (!open) {
setShowModelDialog(false);
setShowCloseConfirm(false);
}
}, [open]);
if (!open || !agent) return null;
const hasNameChanges = name.trim() !== agent.name;
const effectiveModelRef = String(agent.modelRef || defaultModelRef || '').trim();
const modelSummary = effectiveModelRef
? (String(agent.modelDisplay || effectiveModelRef).trim() || effectiveModelRef)
: copy.notConfigured;
function handleRequestClose() {
if (savingName || hasNameChanges) {
setShowCloseConfirm(true);
return;
}
onClose();
}
async function handleSaveName(): Promise<void> {
const trimmedName = name.trim();
if (!trimmedName || trimmedName === agent.name) return;
setSavingName(true);
setErrorMessage(null);
try {
await onUpdateName(agent.id, trimmedName);
onFeedback?.(copy.nameSaved, 'success');
} catch (error) {
setErrorMessage(`${copy.nameSaveFailedPrefix}${error instanceof Error ? error.message : String(error)}`);
} finally {
setSavingName(false);
}
}
return (
<>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div className="flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-3xl bg-[#f3f1e9] shadow-2xl dark:bg-[#1f1f22]">
<div className="flex shrink-0 flex-row items-start justify-between px-6 pb-2 pt-6">
<div>
<h2
className="text-2xl font-normal tracking-tight text-[#171717] dark:text-[#f3f4f6]"
style={{ fontFamily: "Georgia, Cambria, 'Times New Roman', Times, serif" }}
>
{copy.title}
</h2>
<p className="mt-3 text-[15px] text-[#525866] dark:text-gray-400">
{copy.description}
</p>
</div>
<button
type="button"
onClick={handleRequestClose}
className="rounded-full p-1 text-[#99A0AE] transition-colors hover:text-[#171717] dark:hover:text-[#f3f4f6]"
aria-label={copy.closeLabel}
>
<X className="h-5 w-5" />
</button>
</div>
<div className="flex-1 overflow-y-auto p-6 pt-4">
{errorMessage ? (
<div className="mb-5 rounded-[14px] border border-red-500/20 bg-red-500/10 px-4 py-3 text-[13px] text-red-600 dark:text-red-300">
{errorMessage}
</div>
) : null}
<div className="space-y-6">
<div className="space-y-4">
<div className="space-y-2.5">
<label htmlFor="agent-settings-name" className={labelClasses}>{copy.nameLabel}</label>
<div className="flex gap-2">
<input
id="agent-settings-name"
value={name}
onChange={(event) => setName(event.target.value)}
readOnly={agent.isDefault}
className={inputClasses}
/>
{!agent.isDefault ? (
<button
type="button"
onClick={() => {
void handleSaveName();
}}
disabled={savingName || !name.trim() || name.trim() === agent.name}
className="inline-flex h-11 items-center rounded-xl border border-black/10 bg-[#eeece3] px-2 text-[13px] font-medium text-[#171717]/80 transition-colors hover:bg-black/5 hover:text-[#171717] disabled:cursor-not-allowed disabled:opacity-60 dark:border-white/10 dark:bg-[#222225] dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-[#f3f4f6]"
>
{savingName ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
copy.saveLabel
)}
</button>
) : null}
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-1 rounded-2xl border border-transparent bg-black/5 p-4 dark:bg-white/5">
<p className="text-[11px] font-medium uppercase tracking-[0.08em] text-[#7B8190] dark:text-gray-500">
{copy.agentIdLabel}
</p>
<p className="font-mono text-[13px] text-[#171717] dark:text-[#f3f4f6]">{agent.id}</p>
</div>
<button
type="button"
onClick={() => setShowModelDialog(true)}
className="space-y-1 rounded-2xl border border-transparent bg-black/5 p-4 text-left transition-colors hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10"
>
<p className="text-[11px] font-medium uppercase tracking-[0.08em] text-[#7B8190] dark:text-gray-500">
{copy.modelLabel}
</p>
<p className="text-[13.5px] text-[#171717] dark:text-[#f3f4f6]">
{modelSummary}
{agent.inheritedModel ? ` (${copy.inherited})` : ''}
</p>
<p className="break-all font-mono text-[12px] text-[#525866] dark:text-gray-400">
{agent.modelRef || defaultModelRef || '-'}
</p>
</button>
</div>
</div>
<div className="space-y-4">
<div>
<h3
className="text-xl font-normal tracking-tight text-[#171717] dark:text-[#f3f4f6]"
style={{ fontFamily: "Georgia, Cambria, 'Times New Roman', Times, serif" }}
>
{copy.channelsTitle}
</h3>
<p className="mt-1 text-[14px] text-[#525866] dark:text-gray-400">{copy.channelsDescription}</p>
</div>
{assignedChannels.length === 0 && agent.channelTypes.length === 0 ? (
<div className="rounded-2xl border border-dashed border-black/10 bg-black/5 p-4 text-[13.5px] text-[#667085] dark:border-white/10 dark:bg-white/5 dark:text-gray-400">
{copy.noChannels}
</div>
) : (
<div className="space-y-3">
{assignedChannels.map((channel) => (
<div key={`${channel.channelType}-${channel.accountId}`} className="flex items-center justify-between rounded-2xl border border-transparent bg-black/5 p-4 dark:bg-white/5">
<div className="flex min-w-0 items-center gap-3">
<div className="flex h-[40px] w-[40px] shrink-0 items-center justify-center rounded-full border border-black/5 bg-black/5 text-[#171717] shadow-sm dark:border-white/10 dark:bg-white/5 dark:text-[#f3f4f6]">
<ChannelLogo type={channel.channelType} />
</div>
<div className="min-w-0">
<p className="text-[15px] font-semibold text-[#171717] dark:text-[#f3f4f6]">{channel.name}</p>
<p className="text-[13.5px] text-[#667085] dark:text-gray-400">
{channel.channelLabel} · {channel.accountId === 'default' ? copy.mainAccount : channel.accountId}
</p>
{channel.error ? (
<p className="mt-1 text-xs text-red-600 dark:text-red-300">{channel.error}</p>
) : null}
</div>
</div>
<div className="shrink-0" />
</div>
))}
{assignedChannels.length === 0 && agent.channelTypes.length > 0 ? (
<div className="rounded-2xl border border-dashed border-black/10 bg-black/5 p-4 text-[13.5px] text-[#667085] dark:border-white/10 dark:bg-white/5 dark:text-gray-400">
{copy.channelsManagedInChannels}
</div>
) : null}
</div>
)}
</div>
</div>
</div>
</div>
</div>
<AgentModelDialog
open={showModelDialog}
agent={agent}
providerAccounts={providerAccounts}
providerStatuses={providerStatuses}
providerVendors={providerVendors}
providerDefaultAccountId={providerDefaultAccountId}
defaultModelRef={defaultModelRef}
copy={copy}
onClose={() => setShowModelDialog(false)}
onUpdateModel={onUpdateModel}
onFeedback={onFeedback}
/>
<AgentsConfirmDialog
open={showCloseConfirm}
title={copy.unsavedChangesTitle}
message={copy.unsavedChangesMessage}
cancelLabel={copy.cancelLabel}
confirmLabel={copy.closeWithoutSaving}
onClose={() => setShowCloseConfirm(false)}
onConfirm={() => {
setShowCloseConfirm(false);
setName(agent.name);
setErrorMessage(null);
onClose();
}}
/>
</>
);
}