feat(bots): guide event routing and create processors inline

This commit is contained in:
Hyu
2026-10-11 00:44:14 +08:00
parent 8fa5df6c77
commit 4185f0252e
5 changed files with 157 additions and 16 deletions
@@ -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<AgentKind>('pipeline');
const [kind, setKind] = useState<AgentKind>(allowedKinds?.[0] ?? 'pipeline');
const [metadata, setMetadata] = useState<GetAgentMetadataResponseData>();
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 (
<div className="flex h-full flex-col">
<div className="flex items-center justify-between pb-4 shrink-0">
<div className={embedded ? 'order-last flex justify-end border-t pt-4 shrink-0' : 'flex items-center justify-between pb-4 shrink-0'}>
{!embedded && (
<h1 className="text-xl font-semibold">
{t('agents.eventProcessor.createPageTitle')}
</h1>
)}
<Button
type="submit"
form="agent-create-form"
@@ -187,9 +193,9 @@ export default function AgentCreateContent({
>
{t('agents.chooseType')}
</h2>
<p className="mt-1 text-sm text-muted-foreground">
{!embedded && <p className="mt-1 text-sm text-muted-foreground">
{t('agents.chooseTypeDescription')}
</p>
</p>}
</div>
<ToggleGroup
@@ -202,7 +208,7 @@ export default function AgentCreateContent({
spacing={3}
className="grid w-full gap-3 sm:grid-cols-2 lg:grid-cols-1"
>
{typeOptions.map((option) => {
{typeOptions.filter((option) => !allowedKinds || allowedKinds.includes(option.kind)).map((option) => {
const Icon = option.icon;
return (
<div key={option.kind} className="relative w-full">
@@ -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 }));
}}
/>
</div>
@@ -307,7 +313,7 @@ export default function AgentCreateContent({
</Card>
</div>
<Card className="min-h-[600px] overflow-hidden py-0 dark:border-white/16 lg:min-h-[680px]">
<Card className={embedded ? 'min-h-[360px] overflow-hidden py-0 dark:border-white/16 lg:min-h-[480px]' : 'min-h-[600px] overflow-hidden py-0 dark:border-white/16 lg:min-h-[680px]'}>
<CardContent className="flex h-full items-center p-0">
<ProcessorTypeDiagram kind={kind} />
</CardContent>
@@ -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<BotFormHandle, BotFormProps>(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 (
<EntityLoadState error onRetry={() => setLoadAttempt((n) => n + 1)} />
@@ -1231,9 +1242,19 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
data-guide="bot-routing"
className={cn(
'min-w-0',
!hasProcessing && 'pt-0',
initBotId && 'lg:min-h-0 lg:overflow-hidden',
)}
>
{!hasProcessing && (
<div
role="status"
className="flex shrink-0 items-start gap-2.5 rounded-t-xl border-b border-amber-200 bg-amber-50 px-6 py-3 text-sm leading-relaxed text-amber-800 dark:border-amber-800/50 dark:bg-amber-950/40 dark:text-amber-200"
>
<TriangleAlert className="mt-0.5 size-4 shrink-0" aria-hidden="true" />
<p>{t('botSetup.warning')}</p>
</div>
)}
<CardHeader>
<CardTitle className="flex items-center gap-1.5">{t('bots.eventRouting')}<EventRoutingHelp /></CardTitle>
<CardDescription>
@@ -1248,6 +1269,7 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
>
<EventBindingsEditor
form={form}
onAgentCreated={(agent) => setAgentNameList((items) => [...items.filter((item) => item.uuid !== agent.uuid), agent])}
botId={initBotId}
supportedEvents={adapterSupportedEvents[currentAdapter] || []}
agentOptions={agentNameList.filter(
@@ -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<EventBinding>) => 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({
)}
</CommandList>
</Command>
<div className="border-t p-1">
<Button
type="button"
variant="ghost"
className="w-full justify-start"
onClick={() => {
setOpen(false);
setCreating(true);
}}
>
<Plus className="mr-2 size-4" />
{t('botSetup.create')}
</Button>
</div>
</PopoverContent>
</Popover>
<Dialog open={creating} onOpenChange={setCreating}>
<DialogContent
className="flex max-h-[90dvh] w-[calc(100%-2rem)] flex-col overflow-y-auto sm:max-w-6xl"
onSubmit={(event) => event.stopPropagation()}
>
<DialogHeader>
<DialogTitle>{t('botSetup.create')}</DialogTitle>
<DialogDescription>{t('botSetup.createDescription')}</DialogDescription>
</DialogHeader>
<AgentCreateContent
embedded
allowedKinds={pipelineAllowed ? ['pipeline', 'agent'] : ['agent']}
onCreated={async (id) => {
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'));
}
}}
/>
</DialogContent>
</Dialog>
{selectedTarget && (
<Tooltip>
<TooltipTrigger asChild>
@@ -1226,6 +1281,7 @@ interface BindingCardProps {
eventOptions: string[];
agentOptions: Agent[];
onAgentCreated: (agent: Agent) => void;
expandedIds: Set<string>;
onToggleExpand: (id: string) => void;
onUpdate: (globalIndex: number, patch: Partial<EventBinding>) => void;
@@ -1239,6 +1295,7 @@ function BindingCardContent({
globalIndex,
eventOptions,
agentOptions,
onAgentCreated,
expandedIds,
onToggleExpand,
onUpdate,
@@ -1351,6 +1408,7 @@ function BindingCardContent({
<TargetCombobox
binding={binding}
agentOptions={agentOptions}
onAgentCreated={onAgentCreated}
onUpdate={(patch) => 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}
+9 -8
View File
@@ -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',
+50
View File
@@ -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": "สร้างตัวประมวลผลแล้ว แต่ไม่รองรับเหตุการณ์นี้ โปรดเลือกตัวประมวลผลที่รองรับ"
}
};