From 4185f0252ef296529403a4782856a161bd16287d Mon Sep 17 00:00:00 2001 From: Hyu Date: Sun, 11 Oct 2026 00:44:14 +0800 Subject: [PATCH] feat(bots): guide event routing and create processors inline --- .../agents/components/AgentCreateContent.tsx | 22 ++++--- .../home/bots/components/bot-form/BotForm.tsx | 22 +++++++ .../bot-form/EventBindingsEditor.tsx | 62 +++++++++++++++++++ web/src/i18n/index.ts | 17 ++--- web/src/i18n/locales/bot-setup.ts | 50 +++++++++++++++ 5 files changed, 157 insertions(+), 16 deletions(-) create mode 100644 web/src/i18n/locales/bot-setup.ts diff --git a/web/src/app/home/agents/components/AgentCreateContent.tsx b/web/src/app/home/agents/components/AgentCreateContent.tsx index 57cbb062c..ea172e8a6 100644 --- a/web/src/app/home/agents/components/AgentCreateContent.tsx +++ b/web/src/app/home/agents/components/AgentCreateContent.tsx @@ -34,11 +34,15 @@ import { extractI18nObject } from '@/i18n/I18nProvider'; export default function AgentCreateContent({ onCreated, + allowedKinds, + embedded = false, }: { onCreated: (agentId: string) => void; + allowedKinds?: AgentKind[]; + embedded?: boolean; }) { const { t } = useTranslation(); - const [kind, setKind] = useState('pipeline'); + const [kind, setKind] = useState(allowedKinds?.[0] ?? 'pipeline'); const [metadata, setMetadata] = useState(); const [runner, setRunner] = useState(''); const [loading, setLoading] = useState(true); @@ -51,7 +55,7 @@ export default function AgentCreateContent({ }, [t]); useEffect(() => { void loadMetadata(); }, [loadMetadata]); const runnerOptions = kind === 'event_processor' - ? (metadata?.event_processors ?? []).map((item) => ({ name: item.id, label: { en_US: item.id, ...item.label } })) + ? (metadata?.event_processors ?? []).map((item) => ({ name: item.id, label: { en_US: item.id, zh_Hans: item.id, ...item.label } })) : metadata?.runner_config?.stages.find((stage) => stage.name === 'runner')?.config.find((item) => item.name === 'id')?.options ?? []; const selectorLabel = t(kind === 'event_processor' ? 'agents.selectProcessorPlugin' : 'agents.selectAgentRunner'); const formSchema = z.object({ @@ -159,10 +163,12 @@ export default function AgentCreateContent({ ]; return (
-
+
+ {!embedded && (

{t('agents.eventProcessor.createPageTitle')}

+ )}
- {typeOptions.map((option) => { + {typeOptions.filter((option) => !allowedKinds || allowedKinds.includes(option.kind)).map((option) => { const Icon = option.icon; return (
@@ -261,7 +267,7 @@ export default function AgentCreateContent({ }} onEventProcessorInstalled={(component) => { setMetadata((previous) => previous ? { ...previous, event_processors: [...(previous.event_processors ?? []).filter((item) => item.id !== component.id), component] } : previous); - selectRunner(component.id, extractI18nObject({ en_US: component.id, ...component.label })); + selectRunner(component.id, extractI18nObject({ en_US: component.id, zh_Hans: component.id, ...component.label })); }} />
@@ -307,7 +313,7 @@ export default function AgentCreateContent({
- + diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx index dc580fb23..65c0666df 100644 --- a/web/src/app/home/bots/components/bot-form/BotForm.tsx +++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx @@ -27,6 +27,7 @@ import { Agent, Bot } from '@/app/infra/entities/api'; import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs'; import { Cable, + TriangleAlert, Check, ExternalLink, ChevronDown, @@ -715,6 +716,16 @@ const BotForm = forwardRef(function BotForm( }); } + const hasProcessing = + (form.watch('event_bindings') ?? []).some((binding) => + binding.enabled !== false && !!binding.event_pattern && + (binding.target_type === 'discard' || + agentNameList.some((agent) => agent.uuid === binding.target_uuid)), + ) || + (form.watch('plugin_processors') ?? []).some( + (processor) => processor.enabled !== false && !!processor.processor_uuid, + ); + if (loadFailed) return ( setLoadAttempt((n) => n + 1)} /> @@ -1231,9 +1242,19 @@ const BotForm = forwardRef(function BotForm( data-guide="bot-routing" className={cn( 'min-w-0', + !hasProcessing && 'pt-0', initBotId && 'lg:min-h-0 lg:overflow-hidden', )} > + {!hasProcessing && ( +
+
+ )} {t('bots.eventRouting')} @@ -1248,6 +1269,7 @@ const BotForm = forwardRef(function BotForm( > setAgentNameList((items) => [...items.filter((item) => item.uuid !== agent.uuid), agent])} botId={initBotId} supportedEvents={adapterSupportedEvents[currentAdapter] || []} agentOptions={agentNameList.filter( diff --git a/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx b/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx index 29db8f508..c96a364c4 100644 --- a/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx +++ b/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx @@ -119,6 +119,9 @@ import { BotEventRouteStatus, } from '@/app/infra/entities/api'; import { backendClient } from '@/app/infra/http'; +import AgentCreateContent from '@/app/home/agents/components/AgentCreateContent'; +import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext'; +import { toast } from 'sonner'; import { eventGroupLabel, eventNamespaces, @@ -136,6 +139,7 @@ interface EventBindingsEditorProps { botId?: string; supportedEvents: string[]; agentOptions: Agent[]; + onAgentCreated: (agent: Agent) => void; } type FilterField = @@ -496,13 +500,17 @@ function TargetCombobox({ binding, agentOptions, onUpdate, + onAgentCreated, }: { binding: EventBinding; agentOptions: Agent[]; onUpdate: (patch: Partial) => void; + onAgentCreated: (agent: Agent) => void; }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); + const [creating, setCreating] = useState(false); + const { refreshPipelines } = useSidebarData(); const pipelineAllowed = isMessageEventPattern(binding.event_pattern); const targetType = binding.target_type || 'agent'; const selectedTarget = @@ -618,8 +626,55 @@ function TargetCombobox({ )} +
+ +
+ + event.stopPropagation()} + > + + {t('botSetup.create')} + {t('botSetup.createDescription')} + + { + void refreshPipelines(); + try { + const { agent } = await backendClient.getAgent(id); + onAgentCreated(agent); + const compatible = agent.kind === 'pipeline' + ? pipelineAllowed + : agentSupportsEventPattern(agent, binding.event_pattern); + if (compatible) { + onUpdate({ target_type: agent.kind, target_uuid: id }); + } else { + toast.warning(t('botSetup.incompatible')); + } + setCreating(false); + } catch { + toast.error(t('agents.createError')); + } + }} + /> + + {selectedTarget && ( @@ -1226,6 +1281,7 @@ interface BindingCardProps { eventOptions: string[]; agentOptions: Agent[]; + onAgentCreated: (agent: Agent) => void; expandedIds: Set; onToggleExpand: (id: string) => void; onUpdate: (globalIndex: number, patch: Partial) => void; @@ -1239,6 +1295,7 @@ function BindingCardContent({ globalIndex, eventOptions, agentOptions, + onAgentCreated, expandedIds, onToggleExpand, onUpdate, @@ -1351,6 +1408,7 @@ function BindingCardContent({ onUpdate(globalIndex, patch)} /> @@ -1441,6 +1499,7 @@ export default function EventBindingsEditor({ botId, supportedEvents, agentOptions, + onAgentCreated, }: EventBindingsEditorProps) { const { t } = useTranslation(); const watchedBindings: EventBinding[] | undefined = @@ -1696,6 +1755,7 @@ export default function EventBindingsEditor({ globalIndex={globalIdx} eventOptions={eventOptions} agentOptions={agentOptions} + onAgentCreated={onAgentCreated} expandedIds={expandedIds} onToggleExpand={toggleExpand} onUpdate={updateBinding} @@ -1710,6 +1770,7 @@ export default function EventBindingsEditor({ globalIndex={i} eventOptions={eventOptions} agentOptions={agentOptions} + onAgentCreated={onAgentCreated} expandedIds={expandedIds} onToggleExpand={toggleExpand} onUpdate={updateBinding} @@ -1725,6 +1786,7 @@ export default function EventBindingsEditor({ globalIndex={activeGlobalIdx} eventOptions={eventOptions} agentOptions={agentOptions} + onAgentCreated={onAgentCreated} expandedIds={expandedIds} onToggleExpand={toggleExpand} onUpdate={updateBinding} diff --git a/web/src/i18n/index.ts b/web/src/i18n/index.ts index 5e7826821..39c950b9a 100644 --- a/web/src/i18n/index.ts +++ b/web/src/i18n/index.ts @@ -3,6 +3,7 @@ import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import enUS from './locales/en-US'; +import botSetup from './locales/bot-setup'; import zhHans from './locales/zh-Hans'; import zhHant from './locales/zh-Hant'; import jaJP from './locales/ja-JP'; @@ -17,28 +18,28 @@ i18n .init({ resources: { 'en-US': { - translation: enUS, + translation: { ...enUS, botSetup: botSetup['en-US'] }, }, 'zh-Hans': { - translation: zhHans, + translation: { ...zhHans, botSetup: botSetup['zh-Hans'] }, }, 'zh-Hant': { - translation: zhHant, + translation: { ...zhHant, botSetup: botSetup['zh-Hant'] }, }, 'ja-JP': { - translation: jaJP, + translation: { ...jaJP, botSetup: botSetup['ja-JP'] }, }, 'th-TH': { - translation: thTH, + translation: { ...thTH, botSetup: botSetup['th-TH'] }, }, 'vi-VN': { - translation: viVN, + translation: { ...viVN, botSetup: botSetup['vi-VN'] }, }, 'es-ES': { - translation: esES, + translation: { ...esES, botSetup: botSetup['es-ES'] }, }, 'ru-RU': { - translation: ruRU, + translation: { ...ruRU, botSetup: botSetup['ru-RU'] }, }, }, fallbackLng: 'zh-Hans', diff --git a/web/src/i18n/locales/bot-setup.ts b/web/src/i18n/locales/bot-setup.ts new file mode 100644 index 000000000..5f0ccbcc3 --- /dev/null +++ b/web/src/i18n/locales/bot-setup.ts @@ -0,0 +1,50 @@ +export default { + "en-US": { + "warning": "No active event route or plugin processor is configured. This bot will connect but will not automatically reply or process events. Add a rule below and choose a processor.", + "create": "Create new processor", + "createDescription": "Create a processor and select it for this rule. Save the bot to apply the change.", + "incompatible": "The processor was created, but it does not support this event. Choose a compatible processor." + }, + "zh-Hans": { + "warning": "尚未配置已启用的事件路由或插件处理器。机器人连接后不会自动回复或处理事件,请在下方添加规则并选择处理器。", + "create": "创建新处理器", + "createDescription": "创建成功后会选入当前规则,保存机器人后生效。", + "incompatible": "处理器已创建,但不支持当前事件,请选择兼容的处理器。" + }, + "zh-Hant": { + "warning": "尚未設定已啟用的事件路由或外掛處理器。機器人連接後不會自動回覆或處理事件,請在下方新增規則並選擇處理器。", + "create": "建立新處理器", + "createDescription": "建立成功後會選入目前規則,儲存機器人後生效。", + "incompatible": "處理器已建立,但不支援目前事件,請選擇相容的處理器。" + }, + "ja-JP": { + "warning": "有効なイベントルートまたはプラグイン処理が未設定です。接続しても自動返信やイベント処理は行われません。下にルールを追加し、処理を選択してください。", + "create": "新しい処理を作成", + "createDescription": "作成した処理を現在のルールに選択します。ボットを保存すると反映されます。", + "incompatible": "処理は作成されましたが、このイベントに対応していません。対応する処理を選択してください。" + }, + "es-ES": { + "warning": "No hay rutas activas ni procesadores de plugins. El bot se conectará, pero no responderá ni procesará eventos automáticamente. Añade una regla y elige un procesador.", + "create": "Crear procesador", + "createDescription": "El procesador se seleccionará para esta regla. Guarda el bot para aplicar el cambio.", + "incompatible": "El procesador se creó, pero no admite este evento. Elige uno compatible." + }, + "ru-RU": { + "warning": "Нет активных маршрутов или обработчиков плагинов. Бот подключится, но не будет автоматически отвечать или обрабатывать события. Добавьте правило и выберите обработчик.", + "create": "Создать обработчик", + "createDescription": "Созданный обработчик будет выбран для правила. Сохраните бота для применения.", + "incompatible": "Обработчик создан, но не поддерживает это событие. Выберите совместимый обработчик." + }, + "vi-VN": { + "warning": "Chưa có tuyến sự kiện hoặc bộ xử lý plugin đang bật. Bot sẽ kết nối nhưng không tự động trả lời hay xử lý sự kiện. Hãy thêm quy tắc và chọn bộ xử lý.", + "create": "Tạo bộ xử lý mới", + "createDescription": "Bộ xử lý sẽ được chọn cho quy tắc này. Lưu bot để áp dụng.", + "incompatible": "Đã tạo bộ xử lý nhưng không hỗ trợ sự kiện này. Hãy chọn bộ xử lý tương thích." + }, + "th-TH": { + "warning": "ยังไม่มีเส้นทางเหตุการณ์หรือตัวประมวลผลปลั๊กอินที่เปิดใช้งาน บอตจะเชื่อมต่อแต่ไม่ตอบกลับหรือจัดการเหตุการณ์อัตโนมัติ โปรดเพิ่มกฎและเลือกตัวประมวลผลด้านล่าง", + "create": "สร้างตัวประมวลผลใหม่", + "createDescription": "ระบบจะเลือกตัวประมวลผลให้กฎนี้ บันทึกบอตเพื่อใช้การเปลี่ยนแปลง", + "incompatible": "สร้างตัวประมวลผลแล้ว แต่ไม่รองรับเหตุการณ์นี้ โปรดเลือกตัวประมวลผลที่รองรับ" + } +};