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; onUpdateModel: (agentId: string, modelRef: string | null) => Promise | 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, ): 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 Telegram; case 'discord': return Discord; case 'whatsapp': return WhatsApp; case 'wechat': return WeChat; case 'dingtalk': return DingTalk; case 'feishu': return Feishu; case 'wecom': return WeCom; case 'qqbot': return QQ Bot; default: return {type.slice(0, 1).toUpperCase() || 'C'}; } } 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; 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(null); const runtimeProviderOptions = useMemo(() => { const vendorMap = new Map(providerVendors.map((vendor) => [vendor.id, vendor])); const statusById = new Map(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(); 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 { 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 ( <>

{copy.modelLabel}

{copy.modelOverrideDescription.replace('{defaultModel}', defaultModelRef || '-')}

{errorMessage ? (
{errorMessage}
) : null}
{ setModelIdInput(event.target.value); setErrorMessage(null); }} placeholder={selectedProvider?.modelIdPlaceholder || selectedProvider?.configuredModelId || copy.modelIdPlaceholder} className={inputClasses} />
{nextModelRef ? (

{copy.modelPreview}: {nextModelRef}

) : null} {runtimeProviderOptions.length === 0 ? (

{copy.modelProviderEmpty}

) : null}
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(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 { 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 ( <>

{copy.title}

{copy.description}

{errorMessage ? (
{errorMessage}
) : null}
setName(event.target.value)} readOnly={agent.isDefault} className={inputClasses} /> {!agent.isDefault ? ( ) : null}

{copy.agentIdLabel}

{agent.id}

{copy.channelsTitle}

{copy.channelsDescription}

{assignedChannels.length === 0 && agent.channelTypes.length === 0 ? (
{copy.noChannels}
) : (
{assignedChannels.map((channel) => (

{channel.name}

{channel.channelLabel} ยท {channel.accountId === 'default' ? copy.mainAccount : channel.accountId}

{channel.error ? (

{channel.error}

) : null}
))} {assignedChannels.length === 0 && agent.channelTypes.length > 0 ? (
{copy.channelsManagedInChannels}
) : null}
)}
setShowModelDialog(false)} onUpdateModel={onUpdateModel} onFeedback={onFeedback} /> setShowCloseConfirm(false)} onConfirm={() => { setShowCloseConfirm(false); setName(agent.name); setErrorMessage(null); onClose(); }} /> ); }