- 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.
712 lines
27 KiB
TypeScript
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();
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|