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.
This commit is contained in:
@@ -2,10 +2,8 @@
|
|||||||
|
|
||||||
1、任务列表 - 完成
|
1、任务列表 - 完成
|
||||||
2、走本地模型配置,重构模型对话功能 - 完成
|
2、走本地模型配置,重构模型对话功能 - 完成
|
||||||
3、上传表单信息+读取信息,脚本执行录取表单
|
3、一键打开渠道可以新增渠道 - 完成
|
||||||
4、定时任务脚本关联多个脚本执行
|
4、把龙虾包装到对话
|
||||||
5、一键打开渠道可以新增渠道 - 完成
|
5、迁移频道功能 - 完成
|
||||||
6、把龙虾包装到对话
|
6、迁移agent功能 - 完成
|
||||||
7、迁移频道功能
|
7、知识库调整成上传文件,查看文件列表 - 完成
|
||||||
8、迁移agent功能
|
|
||||||
9、知识库调整成上传文件,查看文件列表
|
|
||||||
@@ -120,6 +120,23 @@ export const messages: I18nMessages = {
|
|||||||
saveLabel: 'Save',
|
saveLabel: 'Save',
|
||||||
savingLabel: 'Saving...',
|
savingLabel: 'Saving...',
|
||||||
},
|
},
|
||||||
|
feedback: {
|
||||||
|
created: 'Agent created.',
|
||||||
|
createFailed: 'Failed to create Agent: {error}',
|
||||||
|
deleted: 'Agent deleted.',
|
||||||
|
deleteFailed: 'Failed to delete Agent: {error}',
|
||||||
|
agentUpdated: 'Agent name updated.',
|
||||||
|
agentUpdateFailedPrefix: 'Failed to update Agent name: ',
|
||||||
|
agentModelUpdated: 'Agent model updated.',
|
||||||
|
agentModelReset: 'Agent reverted to the default model.',
|
||||||
|
agentModelUpdateFailedPrefix: 'Failed to update Agent model: ',
|
||||||
|
},
|
||||||
|
deleteDialog: {
|
||||||
|
title: 'Delete Agent',
|
||||||
|
message: 'Delete Agent "{name}"? Existing chats stay on disk.',
|
||||||
|
confirm: 'Delete',
|
||||||
|
deleting: 'Deleting...',
|
||||||
|
},
|
||||||
fields: {
|
fields: {
|
||||||
model: 'Model',
|
model: 'Model',
|
||||||
workspace: 'Workspace',
|
workspace: 'Workspace',
|
||||||
@@ -140,9 +157,11 @@ export const messages: I18nMessages = {
|
|||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
title: 'Agent settings',
|
title: 'Agent settings',
|
||||||
description: 'Review identity, model routing, and channel ownership for {name}.',
|
titleWithName: '{name} settings',
|
||||||
|
description: 'Update the Agent name and review which channel accounts belong to this Agent.',
|
||||||
identityTitle: 'Identity',
|
identityTitle: 'Identity',
|
||||||
modelTitle: 'Model',
|
modelTitle: 'Model',
|
||||||
|
modelLabel: 'Model',
|
||||||
bindingTitle: 'Channel ownership',
|
bindingTitle: 'Channel ownership',
|
||||||
bindingHelp: 'Read-only summary of which channels and accounts currently resolve to this Agent.',
|
bindingHelp: 'Read-only summary of which channels and accounts currently resolve to this Agent.',
|
||||||
bindingEmpty: 'No channels or account ownership are associated with this Agent yet.',
|
bindingEmpty: 'No channels or account ownership are associated with this Agent yet.',
|
||||||
@@ -182,6 +201,27 @@ export const messages: I18nMessages = {
|
|||||||
bindingReadonly: 'Ownership is read-only here. Change channel/account bindings in Channels.',
|
bindingReadonly: 'Ownership is read-only here. Change channel/account bindings in Channels.',
|
||||||
providerLoadError: 'Provider accounts could not be loaded: {error}',
|
providerLoadError: 'Provider accounts could not be loaded: {error}',
|
||||||
channelLoadError: 'Channel accounts could not be loaded: {error}',
|
channelLoadError: 'Channel accounts could not be loaded: {error}',
|
||||||
|
closeLabel: 'Close dialog',
|
||||||
|
notConfigured: 'Not configured',
|
||||||
|
channelsTitle: 'Channels',
|
||||||
|
channelsDescription: 'This list is read-only. Manage channel accounts and bindings from Channels.',
|
||||||
|
noChannels: 'This Agent has not been assigned any channels yet.',
|
||||||
|
channelsManagedInChannels: 'No explicit account bindings were found for this Agent. Review channel-level ownership in Channels.',
|
||||||
|
mainAccount: 'Main account',
|
||||||
|
unsavedChangesTitle: 'Discard unsaved changes?',
|
||||||
|
unsavedChangesMessage: 'You have unsaved edits. Closing now will discard them.',
|
||||||
|
closeWithoutSaving: 'Close without saving',
|
||||||
|
modelOverrideDescription: 'Update this Agent\'s model override. Current default model: {defaultModel}',
|
||||||
|
modelProviderLabel: 'Model provider',
|
||||||
|
modelProviderPlaceholder: 'Select a provider',
|
||||||
|
modelIdLabel: 'Model ID',
|
||||||
|
modelIdPlaceholder: 'gpt-5.4',
|
||||||
|
modelPreview: 'Model preview',
|
||||||
|
modelProviderEmpty: 'No providers are available for model configuration yet. Configure a provider in Models first.',
|
||||||
|
useDefaultModel: 'Use default model',
|
||||||
|
modelProviderRequired: 'Select a provider first.',
|
||||||
|
modelIdRequired: 'Enter a model ID.',
|
||||||
|
modelInvalid: 'The model configuration format is invalid.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
@@ -355,6 +395,23 @@ export const messages: I18nMessages = {
|
|||||||
saveLabel: '保存',
|
saveLabel: '保存',
|
||||||
savingLabel: '保存中...',
|
savingLabel: '保存中...',
|
||||||
},
|
},
|
||||||
|
feedback: {
|
||||||
|
created: 'Agent 已创建。',
|
||||||
|
createFailed: '创建 Agent 失败:{error}',
|
||||||
|
deleted: 'Agent 已删除。',
|
||||||
|
deleteFailed: '删除 Agent 失败:{error}',
|
||||||
|
agentUpdated: 'Agent 名称已更新。',
|
||||||
|
agentUpdateFailedPrefix: '更新 Agent 名称失败:',
|
||||||
|
agentModelUpdated: 'Agent 模型已更新。',
|
||||||
|
agentModelReset: 'Agent 已恢复默认模型。',
|
||||||
|
agentModelUpdateFailedPrefix: '更新 Agent 模型失败:',
|
||||||
|
},
|
||||||
|
deleteDialog: {
|
||||||
|
title: '删除 Agent',
|
||||||
|
message: '确定删除 Agent “{name}”吗?现有聊天记录仍会保留在磁盘中。',
|
||||||
|
confirm: '确认删除',
|
||||||
|
deleting: '删除中...',
|
||||||
|
},
|
||||||
fields: {
|
fields: {
|
||||||
model: '模型',
|
model: '模型',
|
||||||
workspace: '工作区',
|
workspace: '工作区',
|
||||||
@@ -375,9 +432,11 @@ export const messages: I18nMessages = {
|
|||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
title: 'Agent 设置',
|
title: 'Agent 设置',
|
||||||
description: '查看并调整 {name} 的基础信息、模型路由和频道归属摘要。',
|
titleWithName: '{name} 设置',
|
||||||
|
description: '更新 Agent 名称,并管理哪些频道归属于这个 Agent。',
|
||||||
identityTitle: '基础信息',
|
identityTitle: '基础信息',
|
||||||
modelTitle: '模型',
|
modelTitle: '模型',
|
||||||
|
modelLabel: 'Model',
|
||||||
bindingTitle: '频道归属',
|
bindingTitle: '频道归属',
|
||||||
bindingHelp: '这里只展示这个 Agent 当前命中的频道和账号归属摘要。',
|
bindingHelp: '这里只展示这个 Agent 当前命中的频道和账号归属摘要。',
|
||||||
bindingEmpty: '这个 Agent 还没有任何频道或账号归属。',
|
bindingEmpty: '这个 Agent 还没有任何频道或账号归属。',
|
||||||
@@ -417,6 +476,27 @@ export const messages: I18nMessages = {
|
|||||||
bindingReadonly: '这里只读展示归属;修改 channel/account 绑定请前往 Channels。',
|
bindingReadonly: '这里只读展示归属;修改 channel/account 绑定请前往 Channels。',
|
||||||
providerLoadError: '加载 Provider 账号失败:{error}',
|
providerLoadError: '加载 Provider 账号失败:{error}',
|
||||||
channelLoadError: '加载频道账号失败:{error}',
|
channelLoadError: '加载频道账号失败:{error}',
|
||||||
|
closeLabel: '关闭弹窗',
|
||||||
|
notConfigured: '未配置',
|
||||||
|
channelsTitle: '频道',
|
||||||
|
channelsDescription: '该列表为只读。频道账号与绑定关系请在 Channels 页面管理。',
|
||||||
|
noChannels: '这个 Agent 还没有分配任何频道。',
|
||||||
|
channelsManagedInChannels: '该 Agent 当前没有显式账号归属。频道级归属请前往 Channels 页面查看。',
|
||||||
|
mainAccount: '主账号',
|
||||||
|
unsavedChangesTitle: '放弃未保存的更改?',
|
||||||
|
unsavedChangesMessage: '当前有尚未保存的修改。关闭后这些修改将丢失。',
|
||||||
|
closeWithoutSaving: '直接关闭',
|
||||||
|
modelOverrideDescription: '更新这个 Agent 的模型覆盖设置。当前默认模型:{defaultModel}',
|
||||||
|
modelProviderLabel: '模型 Provider',
|
||||||
|
modelProviderPlaceholder: '请选择 Provider',
|
||||||
|
modelIdLabel: '模型 ID',
|
||||||
|
modelIdPlaceholder: 'gpt-5.4',
|
||||||
|
modelPreview: '模型预览',
|
||||||
|
modelProviderEmpty: '当前没有可用于模型配置的 Provider。请先在 Models 页面完成账号配置。',
|
||||||
|
useDefaultModel: '使用默认模型',
|
||||||
|
modelProviderRequired: '请先选择一个 Provider。',
|
||||||
|
modelIdRequired: '请输入模型 ID。',
|
||||||
|
modelInvalid: '模型配置格式不正确。',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
@@ -590,6 +670,23 @@ export const messages: I18nMessages = {
|
|||||||
saveLabel: '保存',
|
saveLabel: '保存',
|
||||||
savingLabel: '保存中...',
|
savingLabel: '保存中...',
|
||||||
},
|
},
|
||||||
|
feedback: {
|
||||||
|
created: 'Agent を作成しました。',
|
||||||
|
createFailed: 'Agent の作成に失敗しました: {error}',
|
||||||
|
deleted: 'Agent を削除しました。',
|
||||||
|
deleteFailed: 'Agent の削除に失敗しました: {error}',
|
||||||
|
agentUpdated: 'Agent 名を更新しました。',
|
||||||
|
agentUpdateFailedPrefix: 'Agent 名の更新に失敗しました: ',
|
||||||
|
agentModelUpdated: 'Agent モデルを更新しました。',
|
||||||
|
agentModelReset: 'Agent を既定モデルに戻しました。',
|
||||||
|
agentModelUpdateFailedPrefix: 'Agent モデルの更新に失敗しました: ',
|
||||||
|
},
|
||||||
|
deleteDialog: {
|
||||||
|
title: 'Agent を削除',
|
||||||
|
message: 'Agent「{name}」を削除しますか?既存のチャットはディスクに残ります。',
|
||||||
|
confirm: '削除する',
|
||||||
|
deleting: '削除中...',
|
||||||
|
},
|
||||||
fields: {
|
fields: {
|
||||||
model: 'モデル',
|
model: 'モデル',
|
||||||
workspace: 'ワークスペース',
|
workspace: 'ワークスペース',
|
||||||
@@ -610,9 +707,11 @@ export const messages: I18nMessages = {
|
|||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
title: 'Agent 設定',
|
title: 'Agent 設定',
|
||||||
description: '{name} の基本情報、モデルルーティング、チャンネル担当の要約を確認します。',
|
titleWithName: '{name} 設定',
|
||||||
|
description: 'Agent 名を更新し、この Agent に属するチャンネルを確認します。',
|
||||||
identityTitle: '基本情報',
|
identityTitle: '基本情報',
|
||||||
modelTitle: 'モデル',
|
modelTitle: 'モデル',
|
||||||
|
modelLabel: 'Model',
|
||||||
bindingTitle: 'チャンネル担当',
|
bindingTitle: 'チャンネル担当',
|
||||||
bindingHelp: 'この Agent に現在解決されるチャンネル / アカウント担当を読み取り専用で表示します。',
|
bindingHelp: 'この Agent に現在解決されるチャンネル / アカウント担当を読み取り専用で表示します。',
|
||||||
bindingEmpty: 'この Agent にはまだチャンネルまたはアカウントの担当がありません。',
|
bindingEmpty: 'この Agent にはまだチャンネルまたはアカウントの担当がありません。',
|
||||||
@@ -652,6 +751,27 @@ export const messages: I18nMessages = {
|
|||||||
bindingReadonly: 'ここでは担当を読み取り専用で表示します。channel/account の変更は Channels ページで行ってください。',
|
bindingReadonly: 'ここでは担当を読み取り専用で表示します。channel/account の変更は Channels ページで行ってください。',
|
||||||
providerLoadError: 'Provider アカウントの読み込みに失敗しました: {error}',
|
providerLoadError: 'Provider アカウントの読み込みに失敗しました: {error}',
|
||||||
channelLoadError: 'チャンネルアカウントの読み込みに失敗しました: {error}',
|
channelLoadError: 'チャンネルアカウントの読み込みに失敗しました: {error}',
|
||||||
|
closeLabel: 'ダイアログを閉じる',
|
||||||
|
notConfigured: '未設定',
|
||||||
|
channelsTitle: 'チャンネル',
|
||||||
|
channelsDescription: 'この一覧は読み取り専用です。チャンネルアカウントと紐付けは Channels ページで管理してください。',
|
||||||
|
noChannels: 'この Agent にはまだチャンネルが割り当てられていません。',
|
||||||
|
channelsManagedInChannels: 'この Agent に明示的なアカウント紐付けは見つかりませんでした。チャンネル単位の担当は Channels で確認してください。',
|
||||||
|
mainAccount: 'メインアカウント',
|
||||||
|
unsavedChangesTitle: '未保存の変更を破棄しますか?',
|
||||||
|
unsavedChangesMessage: '未保存の変更があります。今閉じると破棄されます。',
|
||||||
|
closeWithoutSaving: '保存せずに閉じる',
|
||||||
|
modelOverrideDescription: 'この Agent のモデル上書きを更新します。現在の既定モデル: {defaultModel}',
|
||||||
|
modelProviderLabel: 'モデル Provider',
|
||||||
|
modelProviderPlaceholder: 'Provider を選択',
|
||||||
|
modelIdLabel: 'モデル ID',
|
||||||
|
modelIdPlaceholder: 'gpt-5.4',
|
||||||
|
modelPreview: 'モデルプレビュー',
|
||||||
|
modelProviderEmpty: 'モデル設定に使える Provider がまだありません。先に Models で Provider を設定してください。',
|
||||||
|
useDefaultModel: '既定モデルを使う',
|
||||||
|
modelProviderRequired: '先に Provider を選択してください。',
|
||||||
|
modelIdRequired: 'モデル ID を入力してください。',
|
||||||
|
modelInvalid: 'モデル設定の形式が正しくありません。',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Bot, Check, SlidersHorizontal } from 'lucide-react';
|
import { Bot, Check, Settings2, Trash2 } from 'lucide-react';
|
||||||
import type { AgentSummary } from '@runtime/lib/agents';
|
import type { AgentSummary } from '@runtime/lib/agents';
|
||||||
|
|
||||||
type AgentCardProps = {
|
type AgentCardProps = {
|
||||||
@@ -9,8 +9,10 @@ type AgentCardProps = {
|
|||||||
channelsLabel: string;
|
channelsLabel: string;
|
||||||
channelsValue: string;
|
channelsValue: string;
|
||||||
settingsLabel: string;
|
settingsLabel: string;
|
||||||
|
deleteLabel: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onOpenSettings: (agent: AgentSummary) => void;
|
onOpenSettings: (agent: AgentSummary) => void;
|
||||||
|
onDelete?: (agent: AgentSummary) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AgentCard({
|
export default function AgentCard({
|
||||||
@@ -21,11 +23,13 @@ export default function AgentCard({
|
|||||||
channelsLabel,
|
channelsLabel,
|
||||||
channelsValue,
|
channelsValue,
|
||||||
settingsLabel,
|
settingsLabel,
|
||||||
|
deleteLabel,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
onOpenSettings,
|
onOpenSettings,
|
||||||
|
onDelete,
|
||||||
}: AgentCardProps) {
|
}: AgentCardProps) {
|
||||||
return (
|
return (
|
||||||
<article className="rounded-2xl bg-[#F5F7FA] px-7 py-6 shadow-[inset_0_1px_0_rgba(255,255,255,0.5)] dark:bg-[#1f1f22]">
|
<article className="group rounded-2xl bg-[#F5F7FA] px-7 py-6 shadow-[inset_0_1px_0_rgba(255,255,255,0.5)] dark:bg-[#1f1f22]">
|
||||||
<div className="flex items-center gap-5">
|
<div className="flex items-center gap-5">
|
||||||
<div className="flex h-20.5 w-20.5 shrink-0 items-center justify-center rounded-full bg-[#E3E0D9] text-[#2E67F8] shadow-[0_6px_18px_rgba(15,23,42,0.08)] dark:bg-[#26262a]">
|
<div className="flex h-20.5 w-20.5 shrink-0 items-center justify-center rounded-full bg-[#E3E0D9] text-[#2E67F8] shadow-[0_6px_18px_rgba(15,23,42,0.08)] dark:bg-[#26262a]">
|
||||||
<Bot className="h-9 w-9 stroke-[1.8]" />
|
<Bot className="h-9 w-9 stroke-[1.8]" />
|
||||||
@@ -50,15 +54,29 @@ export default function AgentCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
type="button"
|
{!agent.isDefault && onDelete ? (
|
||||||
disabled={disabled}
|
<button
|
||||||
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-full text-[#6B7CA5] transition-colors hover:bg-white/65 hover:text-[#2E67F8] disabled:cursor-not-allowed disabled:opacity-60 dark:text-gray-400 dark:hover:bg-[#2a2a2d] dark:hover:text-[#8ab4ff]"
|
type="button"
|
||||||
aria-label={`${settingsLabel} ${agent.name}`}
|
disabled={disabled}
|
||||||
onClick={() => onOpenSettings(agent)}
|
className="flex h-11 w-11 items-center justify-center rounded-full text-[#6B7CA5] opacity-0 transition-all hover:bg-red-50 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-60 group-hover:opacity-100 dark:text-gray-400 dark:hover:bg-red-500/10 dark:hover:text-red-300"
|
||||||
>
|
aria-label={`${deleteLabel} ${agent.name}`}
|
||||||
<SlidersHorizontal className="h-5 w-5" />
|
onClick={() => onDelete(agent)}
|
||||||
</button>
|
>
|
||||||
|
<Trash2 className="h-4.5 w-4.5" />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
className="flex h-12 w-12 items-center justify-center rounded-full text-[#6B7CA5] transition-colors hover:bg-white/65 hover:text-[#2E67F8] disabled:cursor-not-allowed disabled:opacity-60 dark:text-gray-400 dark:hover:bg-[#2a2a2d] dark:hover:text-[#8ab4ff]"
|
||||||
|
aria-label={`${settingsLabel} ${agent.name}`}
|
||||||
|
onClick={() => onOpenSettings(agent)}
|
||||||
|
>
|
||||||
|
<Settings2 className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
81
src/pages/Agents/components/AgentsConfirmDialog.tsx
Normal file
81
src/pages/Agents/components/AgentsConfirmDialog.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
|
||||||
|
type AgentsConfirmDialogProps = {
|
||||||
|
open: boolean;
|
||||||
|
busy?: boolean;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
cancelLabel: string;
|
||||||
|
confirmLabel: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AgentsConfirmDialog({
|
||||||
|
open,
|
||||||
|
busy = false,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
cancelLabel,
|
||||||
|
confirmLabel,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
}: AgentsConfirmDialogProps) {
|
||||||
|
function handleOpenChange(nextOpen: boolean) {
|
||||||
|
if (!nextOpen && !busy) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay className="fixed inset-0 z-60 bg-black/45 backdrop-blur-[2px]" />
|
||||||
|
<Dialog.Content
|
||||||
|
className="fixed left-1/2 top-1/2 z-70 w-[calc(100vw-32px)] max-w-126 -translate-x-1/2 -translate-y-1/2 rounded-3xl bg-[#F4F3EB] p-0 shadow-[0_30px_80px_rgba(15,23,42,0.18)] outline-none dark:bg-[#1f1f22]"
|
||||||
|
onEscapeKeyDown={(event) => {
|
||||||
|
if (busy) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onPointerDownOutside={(event) => {
|
||||||
|
if (busy) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="border-b border-black/6 px-6 py-5 dark:border-white/6">
|
||||||
|
<Dialog.Title
|
||||||
|
className="text-[26px] font-normal leading-none tracking-tight text-[#171717] dark:text-[#f3f4f6]"
|
||||||
|
style={{ fontFamily: "Georgia, Cambria, 'Times New Roman', Times, serif" }}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Description className="mt-3 text-[14px] leading-6 text-[#525866] dark:text-gray-400">
|
||||||
|
{message}
|
||||||
|
</Dialog.Description>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end gap-3 px-6 py-5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-10 items-center rounded-full border border-black/10 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-gray-700 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-[#f3f4f6]"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
{cancelLabel}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-10 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"
|
||||||
|
onClick={onConfirm}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
{confirmLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { AlertCircle, Plus, RefreshCw } from 'lucide-react';
|
import { AlertCircle, Plus, RefreshCw } from 'lucide-react';
|
||||||
import type { AgentSummary } from '@runtime/lib/agents';
|
import type { AgentSummary } from '@runtime/lib/agents';
|
||||||
import type { ProviderAccount } from '@runtime/lib/providers';
|
import type { ProviderAccount } from '@runtime/lib/providers';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useI18n } from '../../i18n';
|
import { useI18n } from '../../i18n';
|
||||||
import { onGatewayEvent } from '../../lib/gateway-client';
|
import { onGatewayEvent } from '../../lib/gateway-client';
|
||||||
import { hostApiFetch } from '../../lib/host-api';
|
import { hostApiFetch } from '../../lib/host-api';
|
||||||
|
import type { ChannelAccountCatalogGroup, ChannelAccountsCatalogResponse } from '../../lib/channel-types';
|
||||||
|
import type { ProviderVendorInfo, ProviderWithKeyInfo } from '../../lib/providers';
|
||||||
import { isRuntimeChangedGatewayEvent, runtimeEventHasTopic } from '../../lib/runtime-events';
|
import { isRuntimeChangedGatewayEvent, runtimeEventHasTopic } from '../../lib/runtime-events';
|
||||||
import { agentsStore, useAgentsStore } from '../../stores';
|
import { agentsStore, useAgentsStore } from '../../stores';
|
||||||
import AddAgentDialog from './components/AddAgentDialog';
|
import AddAgentDialog from './components/AddAgentDialog';
|
||||||
import AgentCard from './components/AgentCard';
|
import AgentCard from './components/AgentCard';
|
||||||
|
import AgentsConfirmDialog from './components/AgentsConfirmDialog';
|
||||||
import AgentSettingsDialog from './components/AgentSettingsDialog';
|
import AgentSettingsDialog from './components/AgentSettingsDialog';
|
||||||
|
|
||||||
function interpolateFallback(template: string, params?: Record<string, string | number>): string {
|
function interpolateFallback(template: string, params?: Record<string, string | number>): string {
|
||||||
@@ -34,10 +36,6 @@ function formatChannelLabel(channelType: string): string {
|
|||||||
.join(' ');
|
.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
function countAccountBindings(agentId: string, channelAccountOwners: Record<string, string>): number {
|
|
||||||
return Object.values(channelAccountOwners).filter((ownerId) => ownerId === agentId).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAgentModelValue(
|
function getAgentModelValue(
|
||||||
agent: AgentSummary,
|
agent: AgentSummary,
|
||||||
defaultModelRef: string | null,
|
defaultModelRef: string | null,
|
||||||
@@ -67,8 +65,14 @@ function Spinner() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FeedbackTone = 'success' | 'error' | 'info';
|
||||||
|
type FeedbackState = {
|
||||||
|
id: number;
|
||||||
|
tone: FeedbackTone;
|
||||||
|
message: string;
|
||||||
|
} | null;
|
||||||
|
|
||||||
export default function AgentsPage() {
|
export default function AgentsPage() {
|
||||||
const navigate = useNavigate();
|
|
||||||
const { t, hasMessage } = useI18n();
|
const { t, hasMessage } = useI18n();
|
||||||
const initialized = useAgentsStore((state) => state.initialized);
|
const initialized = useAgentsStore((state) => state.initialized);
|
||||||
const loading = useAgentsStore((state) => state.loading);
|
const loading = useAgentsStore((state) => state.loading);
|
||||||
@@ -77,14 +81,20 @@ export default function AgentsPage() {
|
|||||||
const agents = useAgentsStore((state) => state.agents);
|
const agents = useAgentsStore((state) => state.agents);
|
||||||
const defaultProviderAccountId = useAgentsStore((state) => state.defaultProviderAccountId);
|
const defaultProviderAccountId = useAgentsStore((state) => state.defaultProviderAccountId);
|
||||||
const defaultModelRef = useAgentsStore((state) => state.defaultModelRef);
|
const defaultModelRef = useAgentsStore((state) => state.defaultModelRef);
|
||||||
const channelAccountOwners = useAgentsStore((state) => state.channelAccountOwners);
|
|
||||||
|
|
||||||
|
const feedbackTimerRef = useRef<number | null>(null);
|
||||||
|
const [feedback, setFeedback] = useState<FeedbackState>(null);
|
||||||
const [busyAction, setBusyAction] = useState<string | null>(null);
|
const [busyAction, setBusyAction] = useState<string | null>(null);
|
||||||
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
||||||
const [settingsAgentId, setSettingsAgentId] = useState<string | null>(null);
|
const [settingsAgentId, setSettingsAgentId] = useState<string | null>(null);
|
||||||
|
const [agentToDelete, setAgentToDelete] = useState<AgentSummary | null>(null);
|
||||||
const [providerAccounts, setProviderAccounts] = useState<ProviderAccount[]>([]);
|
const [providerAccounts, setProviderAccounts] = useState<ProviderAccount[]>([]);
|
||||||
|
const [providerStatuses, setProviderStatuses] = useState<ProviderWithKeyInfo[]>([]);
|
||||||
|
const [providerVendors, setProviderVendors] = useState<ProviderVendorInfo[]>([]);
|
||||||
const [providerLoading, setProviderLoading] = useState(false);
|
const [providerLoading, setProviderLoading] = useState(false);
|
||||||
const [providerError, setProviderError] = useState<string | null>(null);
|
const [providerError, setProviderError] = useState<string | null>(null);
|
||||||
|
const [channelGroups, setChannelGroups] = useState<ChannelAccountCatalogGroup[]>([]);
|
||||||
|
const [channelLoading, setChannelLoading] = useState(false);
|
||||||
|
|
||||||
const message = (path: string, fallback: string, params?: Record<string, string | number>) => (
|
const message = (path: string, fallback: string, params?: Record<string, string | number>) => (
|
||||||
hasMessage(path)
|
hasMessage(path)
|
||||||
@@ -92,19 +102,47 @@ export default function AgentsPage() {
|
|||||||
: interpolateFallback(fallback, params)
|
: interpolateFallback(fallback, params)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function pushFeedback(nextMessage: string, tone: FeedbackTone = 'info') {
|
||||||
|
const nextFeedback = { id: Date.now(), tone, message: nextMessage };
|
||||||
|
setFeedback(nextFeedback);
|
||||||
|
|
||||||
|
if (feedbackTimerRef.current) {
|
||||||
|
window.clearTimeout(feedbackTimerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
feedbackTimerRef.current = window.setTimeout(() => {
|
||||||
|
setFeedback((current) => (current?.id === nextFeedback.id ? null : current));
|
||||||
|
}, 3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (feedbackTimerRef.current) {
|
||||||
|
window.clearTimeout(feedbackTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void agentsStore.init();
|
void agentsStore.init();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadProviderAccounts();
|
void Promise.allSettled([
|
||||||
|
loadProviderCatalog(),
|
||||||
|
loadChannelGroups(),
|
||||||
|
]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => (
|
useEffect(() => (
|
||||||
onGatewayEvent((event) => {
|
onGatewayEvent((event) => {
|
||||||
if (!isRuntimeChangedGatewayEvent(event)) return;
|
if (!isRuntimeChangedGatewayEvent(event)) return;
|
||||||
if (!runtimeEventHasTopic(event, 'providers', 'models', 'agents')) return;
|
if (!runtimeEventHasTopic(event, 'providers', 'models', 'agents', 'channels')) return;
|
||||||
void loadProviderAccounts(false);
|
|
||||||
|
void Promise.allSettled([
|
||||||
|
loadProviderCatalog(false),
|
||||||
|
loadChannelGroups(false),
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
), []);
|
), []);
|
||||||
|
|
||||||
@@ -122,25 +160,29 @@ export default function AgentsPage() {
|
|||||||
[settingsAgentId, sortedAgents],
|
[settingsAgentId, sortedAgents],
|
||||||
);
|
);
|
||||||
|
|
||||||
const mainAgent = useMemo(
|
|
||||||
() => sortedAgents.find((agent) => agent.isDefault) ?? null,
|
|
||||||
[sortedAgents],
|
|
||||||
);
|
|
||||||
|
|
||||||
const isBusy = busyAction !== null;
|
const isBusy = busyAction !== null;
|
||||||
const isInitialLoading = loading && !initialized;
|
const isInitialLoading = loading && !initialized;
|
||||||
|
|
||||||
async function loadProviderAccounts(showLoading = true): Promise<void> {
|
async function loadProviderCatalog(showLoading = true): Promise<void> {
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
setProviderLoading(true);
|
setProviderLoading(true);
|
||||||
}
|
}
|
||||||
setProviderError(null);
|
setProviderError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const accounts = await hostApiFetch<ProviderAccount[]>('/api/provider-accounts');
|
const [accounts, statuses, vendors] = await Promise.all([
|
||||||
|
hostApiFetch<ProviderAccount[]>('/api/provider-accounts'),
|
||||||
|
hostApiFetch<ProviderWithKeyInfo[]>('/api/providers'),
|
||||||
|
hostApiFetch<ProviderVendorInfo[]>('/api/provider-vendors'),
|
||||||
|
]);
|
||||||
|
|
||||||
setProviderAccounts(Array.isArray(accounts) ? accounts.filter((account) => account?.enabled !== false) : []);
|
setProviderAccounts(Array.isArray(accounts) ? accounts.filter((account) => account?.enabled !== false) : []);
|
||||||
|
setProviderStatuses(Array.isArray(statuses) ? statuses : []);
|
||||||
|
setProviderVendors(Array.isArray(vendors) ? vendors : []);
|
||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
setProviderAccounts([]);
|
setProviderAccounts([]);
|
||||||
|
setProviderStatuses([]);
|
||||||
|
setProviderVendors([]);
|
||||||
setProviderError(requestError instanceof Error ? requestError.message : String(requestError));
|
setProviderError(requestError instanceof Error ? requestError.message : String(requestError));
|
||||||
} finally {
|
} finally {
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
@@ -149,10 +191,30 @@ export default function AgentsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadChannelGroups(showLoading = true): Promise<void> {
|
||||||
|
if (showLoading) {
|
||||||
|
setChannelLoading(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await hostApiFetch<ChannelAccountsCatalogResponse>('/api/channels/accounts');
|
||||||
|
setChannelGroups(Array.isArray(response.channels) ? response.channels : []);
|
||||||
|
} catch {
|
||||||
|
if (showLoading) {
|
||||||
|
setChannelGroups([]);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (showLoading) {
|
||||||
|
setChannelLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleRefresh(): Promise<void> {
|
async function handleRefresh(): Promise<void> {
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
agentsStore.refresh(),
|
agentsStore.refresh(),
|
||||||
loadProviderAccounts(),
|
loadProviderCatalog(),
|
||||||
|
loadChannelGroups(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,52 +223,36 @@ export default function AgentsPage() {
|
|||||||
try {
|
try {
|
||||||
await agentsStore.createAgent(input.name, { inheritWorkspace: input.inheritWorkspace });
|
await agentsStore.createAgent(input.name, { inheritWorkspace: input.inheritWorkspace });
|
||||||
setAddDialogOpen(false);
|
setAddDialogOpen(false);
|
||||||
|
pushFeedback(message('agents.feedback.created', 'Agent 已创建。'), 'success');
|
||||||
|
} catch (createError) {
|
||||||
|
pushFeedback(
|
||||||
|
message('agents.feedback.createFailed', '创建 Agent 失败:{error}', {
|
||||||
|
error: createError instanceof Error ? createError.message : String(createError),
|
||||||
|
}),
|
||||||
|
'error',
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setBusyAction(null);
|
setBusyAction(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveSettings(input: { name: string; providerAccountId: string | null; modelRef: string | null }): Promise<void> {
|
async function handleConfirmDeleteAgent(): Promise<void> {
|
||||||
if (!settingsAgent || settingsAgent.isDefault) {
|
if (!agentToDelete) return;
|
||||||
setSettingsAgentId(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setBusyAction(`save:${settingsAgent.id}`);
|
const deletingAgent = agentToDelete;
|
||||||
|
setBusyAction(`delete:${deletingAgent.id}`);
|
||||||
try {
|
try {
|
||||||
if (input.name.trim() && input.name.trim() !== settingsAgent.name) {
|
await agentsStore.deleteAgent(deletingAgent.id);
|
||||||
await agentsStore.updateAgent(settingsAgent.id, input.name.trim());
|
setSettingsAgentId((current) => (current === deletingAgent.id ? null : current));
|
||||||
}
|
setAgentToDelete(null);
|
||||||
|
pushFeedback(message('agents.feedback.deleted', 'Agent 已删除。'), 'success');
|
||||||
if (
|
} catch (deleteError) {
|
||||||
input.providerAccountId !== (settingsAgent.providerAccountId ?? null)
|
pushFeedback(
|
||||||
|| input.modelRef !== (settingsAgent.overrideModelRef ?? null)
|
message('agents.feedback.deleteFailed', '删除 Agent 失败:{error}', {
|
||||||
) {
|
error: deleteError instanceof Error ? deleteError.message : String(deleteError),
|
||||||
await agentsStore.updateAgentModel(settingsAgent.id, input.modelRef, {
|
}),
|
||||||
providerAccountId: input.providerAccountId,
|
'error',
|
||||||
});
|
);
|
||||||
}
|
|
||||||
|
|
||||||
setSettingsAgentId(null);
|
|
||||||
} finally {
|
|
||||||
setBusyAction(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDeleteAgent(agent: AgentSummary): Promise<void> {
|
|
||||||
const confirmed = window.confirm(
|
|
||||||
message(
|
|
||||||
'agents.prompts.deleteConfirm',
|
|
||||||
'确定删除 Agent “{name}”吗?已有会话记录会保留在磁盘上,但它会从 Agents 控制台中移除。',
|
|
||||||
{ name: agent.name },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (!confirmed) return;
|
|
||||||
|
|
||||||
setBusyAction(`delete:${agent.id}`);
|
|
||||||
try {
|
|
||||||
await agentsStore.deleteAgent(agent.id);
|
|
||||||
setSettingsAgentId((current) => (current === agent.id ? null : current));
|
|
||||||
} finally {
|
} finally {
|
||||||
setBusyAction(null);
|
setBusyAction(null);
|
||||||
}
|
}
|
||||||
@@ -232,6 +278,7 @@ export default function AgentsPage() {
|
|||||||
modelLabel: message('agents.card.modelLabel', 'Model'),
|
modelLabel: message('agents.card.modelLabel', 'Model'),
|
||||||
channelsLabel: message('agents.fields.channels', '频道'),
|
channelsLabel: message('agents.fields.channels', '频道'),
|
||||||
settingsLabel: message('agents.actions.settings', '设置'),
|
settingsLabel: message('agents.actions.settings', '设置'),
|
||||||
|
deleteLabel: message('agents.actions.delete', '删除'),
|
||||||
emptyTitle: message('agents.emptyTitle', '暂无 Agent'),
|
emptyTitle: message('agents.emptyTitle', '暂无 Agent'),
|
||||||
emptyDescription: message('agents.emptyDescription', '创建新的 Agent 后,这里会显示对应的卡片摘要。'),
|
emptyDescription: message('agents.emptyDescription', '创建新的 Agent 后,这里会显示对应的卡片摘要。'),
|
||||||
notConfigured: message('agents.card.missingModel', 'Not configured'),
|
notConfigured: message('agents.card.missingModel', 'Not configured'),
|
||||||
@@ -247,39 +294,50 @@ export default function AgentsPage() {
|
|||||||
savingLabel: message('agents.createDialog.savingLabel', '保存中...'),
|
savingLabel: message('agents.createDialog.savingLabel', '保存中...'),
|
||||||
},
|
},
|
||||||
settingsDialog: {
|
settingsDialog: {
|
||||||
title: message('agents.settings.title', 'Agent 设置'),
|
title: settingsAgent
|
||||||
subtitle: settingsAgent
|
? message('agents.settings.titleWithName', '{name} 设置', { name: settingsAgent.name })
|
||||||
? message('agents.settings.description', '查看并调整 {name} 的基础信息、模型路由和频道归属摘要。', { name: settingsAgent.name })
|
: message('agents.settings.title', 'Agent 设置'),
|
||||||
: '',
|
description: message('agents.settings.description', '更新 Agent 名称,并管理哪些频道归属于这个 Agent。'),
|
||||||
identityTitle: message('agents.settings.identityTitle', '基础信息'),
|
|
||||||
nameLabel: message('agents.settings.nameLabel', 'Agent 名称'),
|
nameLabel: message('agents.settings.nameLabel', 'Agent 名称'),
|
||||||
agentIdLabel: message('agents.settings.agentIdLabel', 'Agent ID'),
|
|
||||||
workspaceTitle: message('agents.settings.workspaceTitle', '工作区'),
|
|
||||||
workspaceDescription: message('agents.settings.workspaceDescription', '查看当前 Agent 的工作区路径,以及是否继承主 Agent 工作区。'),
|
|
||||||
workspaceLabel: message('agents.fields.workspace', '工作区'),
|
|
||||||
inheritedWorkspaceLabel: message('agents.settings.inheritedWorkspaceLabel', '继承主 Agent 工作区'),
|
|
||||||
inheritedWorkspaceYes: message('agents.settings.inheritedWorkspaceYes', '是'),
|
|
||||||
inheritedWorkspaceNo: message('agents.settings.inheritedWorkspaceNo', '否'),
|
|
||||||
modelTitle: message('agents.settings.modelTitle', '模型'),
|
|
||||||
providerAccountLabel: message('agents.settings.providerAccountLabel', 'Provider 账号'),
|
|
||||||
useDefaultProvider: message('agents.settings.useDefaultProvider', '使用工作区默认 Provider'),
|
|
||||||
modelRefLabel: message('agents.settings.modelRefLabel', '模型覆盖'),
|
|
||||||
modelRefPlaceholder: message('agents.settings.modelRefPlaceholder', 'provider/model-id'),
|
|
||||||
effectiveProviderLabel: message('agents.settings.effectiveProviderLabel', '生效中的 Provider'),
|
|
||||||
effectiveModelLabel: message('agents.settings.effectiveModelLabel', '生效中的模型'),
|
|
||||||
modelHelp: message('agents.settings.modelHelp', '留空后会跟随所选 Provider 的模型;如果没有固定 Provider,则继续继承工作区默认模型。'),
|
|
||||||
managedFromModels: message('agents.settings.managedFromModels', 'Main Agent 使用 Models 页面里配置的 Provider 和默认模型。'),
|
|
||||||
openModelsLabel: message('agents.settings.openModels', '前往 Models'),
|
|
||||||
channelsTitle: message('agents.settings.bindingTitle', '频道归属'),
|
|
||||||
channelSummaryLabel: message('agents.settings.channelSummaryLabel', '频道路由摘要'),
|
|
||||||
accountBindingsLabel: message('agents.settings.accountBindingsLabel', '账号绑定数'),
|
|
||||||
noChannels: message('agents.card.noChannels', '无'),
|
|
||||||
openChannelsLabel: message('agents.settings.manageBindings', '前往 Channels'),
|
|
||||||
providerLoadErrorPrefix: message('agents.settings.providerLoadErrorPrefix', '加载 Provider 账号失败:'),
|
|
||||||
cancelLabel: message('dialog.cancel', '取消'),
|
|
||||||
saveLabel: message('agents.settings.save', '保存'),
|
saveLabel: message('agents.settings.save', '保存'),
|
||||||
savingLabel: message('agents.settings.saving', '保存中...'),
|
savingLabel: message('agents.settings.saving', '保存中...'),
|
||||||
deleteLabel: message('agents.actions.delete', '删除'),
|
closeLabel: message('agents.settings.closeLabel', '关闭弹窗'),
|
||||||
|
agentIdLabel: message('agents.settings.agentIdLabel', 'Agent ID'),
|
||||||
|
modelLabel: message('agents.settings.modelLabel', 'Model'),
|
||||||
|
notConfigured: message('agents.settings.notConfigured', 'Not configured'),
|
||||||
|
inherited: message('agents.inherited', 'Inherited'),
|
||||||
|
channelsTitle: message('agents.settings.channelsTitle', '频道'),
|
||||||
|
channelsDescription: message('agents.settings.channelsDescription', '该列表为只读。频道账号与绑定关系请在 Channels 页面管理。'),
|
||||||
|
noChannels: message('agents.settings.noChannels', '这个 Agent 还没有分配任何频道。'),
|
||||||
|
channelsManagedInChannels: message('agents.settings.channelsManagedInChannels', '该 Agent 当前没有显式账号归属。频道级归属请前往 Channels 页面查看。'),
|
||||||
|
mainAccount: message('agents.settings.mainAccount', '主账号'),
|
||||||
|
cancelLabel: message('dialog.cancel', '取消'),
|
||||||
|
unsavedChangesTitle: message('agents.settings.unsavedChangesTitle', '放弃未保存的更改?'),
|
||||||
|
unsavedChangesMessage: message('agents.settings.unsavedChangesMessage', '当前有尚未保存的修改。关闭后这些修改将丢失。'),
|
||||||
|
closeWithoutSaving: message('agents.settings.closeWithoutSaving', '直接关闭'),
|
||||||
|
modelOverrideDescription: message('agents.settings.modelOverrideDescription', '更新这个 Agent 的模型覆盖设置。当前默认模型:{defaultModel}'),
|
||||||
|
modelProviderLabel: message('agents.settings.modelProviderLabel', '模型 Provider'),
|
||||||
|
modelProviderPlaceholder: message('agents.settings.modelProviderPlaceholder', '请选择 Provider'),
|
||||||
|
modelIdLabel: message('agents.settings.modelIdLabel', '模型 ID'),
|
||||||
|
modelIdPlaceholder: message('agents.settings.modelIdPlaceholder', 'gpt-5.4'),
|
||||||
|
modelPreview: message('agents.settings.modelPreview', '模型预览'),
|
||||||
|
modelProviderEmpty: message('agents.settings.modelProviderEmpty', '当前没有可用于模型配置的 Provider。请先在 Models 页面完成账号配置。'),
|
||||||
|
useDefaultModel: message('agents.settings.useDefaultModel', '使用默认模型'),
|
||||||
|
modelProviderRequired: message('agents.settings.modelProviderRequired', '请选择一个 Provider。'),
|
||||||
|
modelIdRequired: message('agents.settings.modelIdRequired', '请输入模型 ID。'),
|
||||||
|
modelInvalid: message('agents.settings.modelInvalid', '模型配置格式不正确。'),
|
||||||
|
nameSaved: message('agents.feedback.agentUpdated', 'Agent 名称已更新。'),
|
||||||
|
nameSaveFailedPrefix: message('agents.feedback.agentUpdateFailedPrefix', '更新 Agent 名称失败:'),
|
||||||
|
modelSaved: message('agents.feedback.agentModelUpdated', 'Agent 模型已更新。'),
|
||||||
|
modelReset: message('agents.feedback.agentModelReset', 'Agent 已恢复默认模型。'),
|
||||||
|
modelSaveFailedPrefix: message('agents.feedback.agentModelUpdateFailedPrefix', '更新 Agent 模型失败:'),
|
||||||
|
},
|
||||||
|
deleteDialog: {
|
||||||
|
title: message('agents.deleteDialog.title', '删除 Agent'),
|
||||||
|
message: agentToDelete
|
||||||
|
? message('agents.deleteDialog.message', '确定删除 Agent “{name}”吗?现有聊天记录仍会保留在磁盘中。', { name: agentToDelete.name })
|
||||||
|
: '',
|
||||||
|
confirmLabel: busyAction && agentToDelete ? message('agents.deleteDialog.deleting', '删除中...') : message('agents.deleteDialog.confirm', '确认删除'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -303,19 +361,19 @@ export default function AgentsPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
className="inline-flex px-4 py-2 items-center gap-3 rounded-full border border-black/12 bg-white/50 text-[18px] font-medium text-[#2E3445] transition-colors hover:bg-white/80 disabled:cursor-not-allowed disabled:opacity-60 dark:border-white/10 dark:bg-[#222225] dark:text-gray-200 dark:hover:bg-[#2a2a2d]"
|
className="inline-flex items-center gap-3 rounded-full border border-black/12 bg-white/50 px-4 py-2 text-[18px] font-medium text-[#2E3445] transition-colors hover:bg-white/80 disabled:cursor-not-allowed disabled:opacity-60 dark:border-white/10 dark:bg-[#222225] dark:text-gray-200 dark:hover:bg-[#2a2a2d]"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void handleRefresh();
|
void handleRefresh();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RefreshCw className={['h-5 w-5', (loading || providerLoading) ? 'animate-spin' : ''].join(' ')} />
|
<RefreshCw className={['h-5 w-5', (loading || providerLoading || channelLoading) ? 'animate-spin' : ''].join(' ')} />
|
||||||
{pageCopy.refresh}
|
{pageCopy.refresh}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
className="inline-flex px-4 py-2 items-center gap-3 rounded-full bg-[#2E67F8] text-[18px] font-semibold text-white transition-colors hover:bg-[#2458E0] disabled:cursor-not-allowed disabled:opacity-60"
|
className="inline-flex items-center gap-3 rounded-full bg-[#2E67F8] px-4 py-2 text-[18px] font-semibold text-white transition-colors hover:bg-[#2458E0] disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAddDialogOpen(true);
|
setAddDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
@@ -327,6 +385,19 @@ export default function AgentsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-10 space-y-4">
|
<div className="mt-10 space-y-4">
|
||||||
|
{feedback ? (
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
'rounded-3xl border px-5 py-4 text-[15px]',
|
||||||
|
feedback.tone === 'success' ? 'border-emerald-500/25 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300' : '',
|
||||||
|
feedback.tone === 'error' ? 'border-red-500/25 bg-red-500/10 text-red-700 dark:text-red-300' : '',
|
||||||
|
feedback.tone === 'info' ? 'border-black/10 bg-white text-[#525866] dark:border-gray-700 dark:bg-[#222225] dark:text-gray-300' : '',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
{feedback.message}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{warning ? (
|
{warning ? (
|
||||||
<div className="rounded-3xl border border-amber-500/25 bg-amber-500/10 px-5 py-4 text-[15px] text-amber-700 dark:text-amber-300">
|
<div className="rounded-3xl border border-amber-500/25 bg-amber-500/10 px-5 py-4 text-[15px] text-amber-700 dark:text-amber-300">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
@@ -349,7 +420,7 @@ export default function AgentsPage() {
|
|||||||
<div className="rounded-3xl border border-amber-500/25 bg-amber-500/10 px-5 py-4 text-[15px] text-amber-700 dark:text-amber-300">
|
<div className="rounded-3xl border border-amber-500/25 bg-amber-500/10 px-5 py-4 text-[15px] text-amber-700 dark:text-amber-300">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<AlertCircle className="mt-0.5 h-5 w-5 shrink-0" />
|
<AlertCircle className="mt-0.5 h-5 w-5 shrink-0" />
|
||||||
<span>{`${pageCopy.settingsDialog.providerLoadErrorPrefix}${providerError}`}</span>
|
<span>{`${message('agents.settings.providerLoadErrorPrefix', '加载 Provider 账号失败:')}${providerError}`}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -376,8 +447,10 @@ export default function AgentsPage() {
|
|||||||
channelsLabel={pageCopy.channelsLabel}
|
channelsLabel={pageCopy.channelsLabel}
|
||||||
channelsValue={getAgentChannelsValue(agent, pageCopy.none)}
|
channelsValue={getAgentChannelsValue(agent, pageCopy.none)}
|
||||||
settingsLabel={pageCopy.settingsLabel}
|
settingsLabel={pageCopy.settingsLabel}
|
||||||
|
deleteLabel={pageCopy.deleteLabel}
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
onOpenSettings={(targetAgent) => setSettingsAgentId(targetAgent.id)}
|
onOpenSettings={(targetAgent) => setSettingsAgentId(targetAgent.id)}
|
||||||
|
onDelete={agent.isDefault ? undefined : (targetAgent) => setAgentToDelete(targetAgent)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -399,26 +472,34 @@ export default function AgentsPage() {
|
|||||||
<AgentSettingsDialog
|
<AgentSettingsDialog
|
||||||
open={Boolean(settingsAgent)}
|
open={Boolean(settingsAgent)}
|
||||||
agent={settingsAgent}
|
agent={settingsAgent}
|
||||||
|
channelGroups={channelGroups}
|
||||||
providerAccounts={providerAccounts}
|
providerAccounts={providerAccounts}
|
||||||
providerLoading={providerLoading}
|
providerStatuses={providerStatuses}
|
||||||
providerError={providerError}
|
providerVendors={providerVendors}
|
||||||
defaultProviderAccountId={defaultProviderAccountId}
|
providerDefaultAccountId={defaultProviderAccountId}
|
||||||
defaultModelRef={defaultModelRef}
|
defaultModelRef={defaultModelRef}
|
||||||
mainWorkspacePath={mainAgent?.workspace ?? null}
|
|
||||||
channelSummary={settingsAgent ? getAgentChannelsValue(settingsAgent, pageCopy.none) : pageCopy.none}
|
|
||||||
accountBindingCount={settingsAgent ? countAccountBindings(settingsAgent.id, channelAccountOwners) : 0}
|
|
||||||
saving={Boolean(settingsAgent && busyAction === `save:${settingsAgent.id}`)}
|
|
||||||
deleting={Boolean(settingsAgent && busyAction === `delete:${settingsAgent.id}`)}
|
|
||||||
copy={pageCopy.settingsDialog}
|
copy={pageCopy.settingsDialog}
|
||||||
|
onClose={() => setSettingsAgentId(null)}
|
||||||
|
onUpdateName={(agentId, name) => agentsStore.updateAgent(agentId, name)}
|
||||||
|
onUpdateModel={(agentId, modelRef) => agentsStore.updateAgentModel(agentId, modelRef)}
|
||||||
|
onFeedback={pushFeedback}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AgentsConfirmDialog
|
||||||
|
open={!!agentToDelete}
|
||||||
|
busy={Boolean(agentToDelete && busyAction === `delete:${agentToDelete.id}`)}
|
||||||
|
title={pageCopy.deleteDialog.title}
|
||||||
|
message={pageCopy.deleteDialog.message}
|
||||||
|
cancelLabel={message('dialog.cancel', '取消')}
|
||||||
|
confirmLabel={pageCopy.deleteDialog.confirmLabel}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
if (!isBusy) {
|
if (!busyAction) {
|
||||||
setSettingsAgentId(null);
|
setAgentToDelete(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onSave={handleSaveSettings}
|
onConfirm={() => {
|
||||||
onDelete={handleDeleteAgent}
|
void handleConfirmDeleteAgent();
|
||||||
onOpenChannels={() => navigate('/channels')}
|
}}
|
||||||
onOpenModels={() => navigate('/models')}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user