feat(bots): refine event binding editor UI and i18n

- Move conditions toggle between event select and arrow; drop "IF" label
- Remove "all events" (*) option from event select
- Add i18n labels for concrete event names (zh/en/ja) via bots.eventNames
- Narrow fallback event set to message.received for adapters without
  declared supported_events (legacy adapters only emit message.received)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Junyan Qin
2026-06-26 19:52:01 +08:00
parent a5acf41df1
commit f157174ae4
4 changed files with 85 additions and 35 deletions
@@ -2,6 +2,7 @@
import { useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import { UseFormReturn } from 'react-hook-form';
import {
ArrowRight,
@@ -114,15 +115,9 @@ const ELEMENTS = [
'Quote',
];
const DEFAULT_EVENTS = [
'message.received',
'feedback.received',
'group.member_joined',
'group.member_left',
'friend.request_received',
'bot.invited_to_group',
'platform.specific',
];
// Adapters that don't declare `supported_events` (e.g. legacy adapters)
// only emit message.received, so that's the sole fallback option.
const DEFAULT_EVENTS = ['message.received'];
// ── helpers ───────────────────────────────────────────────────────────────────
@@ -156,6 +151,20 @@ function eventNamespaces(events: string[]) {
return Array.from(ns).sort();
}
// Localized label for an event pattern. Concrete events look up
// `bots.eventNames.<event_with_underscores>`, falling back to the raw
// string when no translation exists (e.g. custom/unknown events).
function eventLabel(event: string, t: TFunction) {
if (event === '*') return t('bots.eventWildcard');
if (event.endsWith('.*'))
return t('bots.eventNamespaceWildcard', {
namespace: event.replace('.*', ''),
});
const key = `bots.eventNames.${event.replace(/\./g, '_')}`;
const label = t(key);
return label === key ? event : label;
}
function targetLabel(agent: Agent) {
return `${agent.emoji ? `${agent.emoji} ` : ''}${agent.name}`;
}
@@ -509,10 +518,6 @@ function BindingCardContent({
</button>
)}
<span className="text-xs font-medium text-muted-foreground shrink-0">
IF
</span>
<Select
value={binding.event_pattern}
onValueChange={(eventPattern) => {
@@ -535,32 +540,12 @@ function BindingCardContent({
<SelectContent>
{eventOptions.map((event) => (
<SelectItem key={event} value={event}>
{event === '*'
? t('bots.eventWildcard')
: event.endsWith('.*')
? t('bots.eventNamespaceWildcard', {
namespace: event.replace('.*', ''),
})
: event}
{eventLabel(event, t)}
</SelectItem>
))}
</SelectContent>
</Select>
<ArrowRight className="h-4 w-4 shrink-0 text-muted-foreground" />
<TargetCombobox
binding={binding}
agentOptions={agentOptions}
onUpdate={(patch) => onUpdate(globalIndex, patch)}
/>
{!pipelineAllowed && binding.target_type === 'pipeline' && (
<span className="text-xs text-destructive shrink-0">
{t('bots.unsupportedPipelineEvent')}
</span>
)}
{/* conditions toggle */}
<Button
type="button"
@@ -582,6 +567,20 @@ function BindingCardContent({
)}
</Button>
<ArrowRight className="h-4 w-4 shrink-0 text-muted-foreground" />
<TargetCombobox
binding={binding}
agentOptions={agentOptions}
onUpdate={(patch) => onUpdate(globalIndex, patch)}
/>
{!pipelineAllowed && binding.target_type === 'pipeline' && (
<span className="text-xs text-destructive shrink-0">
{t('bots.unsupportedPipelineEvent')}
</span>
)}
{/* disable/enable toggle */}
<Button
type="button"
@@ -669,7 +668,7 @@ export default function EventBindingsEditor({
const eventOptions = useMemo(() => {
const concrete =
supportedEvents.length > 0 ? supportedEvents : DEFAULT_EVENTS;
return ['*', ...eventNamespaces(concrete), ...concrete].filter(
return [...eventNamespaces(concrete), ...concrete].filter(
(e, i, a) => a.indexOf(e) === i,
);
}, [supportedEvents]);
+17
View File
@@ -388,6 +388,23 @@ const enUS = {
eventCustom: 'Custom event',
eventWildcard: 'All events',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
message_received: 'Message received',
message_edited: 'Message edited',
message_deleted: 'Message deleted',
message_reaction: 'Message reaction',
feedback_received: 'Feedback received',
friend_request_received: 'Friend request received',
friend_added: 'Friend added',
group_member_joined: 'Member joined group',
group_member_left: 'Member left group',
group_member_banned: 'Member banned',
bot_invited_to_group: 'Bot invited to group',
bot_removed_from_group: 'Bot removed from group',
bot_muted: 'Bot muted',
bot_unmuted: 'Bot unmuted',
platform_specific: 'Platform-specific event',
},
conditions: 'Conditions',
conditionsDescription:
'All conditions must match to trigger this binding. Leave empty to always trigger.',
+17
View File
@@ -387,6 +387,23 @@ const jaJP = {
eventCustom: 'カスタムイベント',
eventWildcard: 'すべてのイベント',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
message_received: 'メッセージ受信',
message_edited: 'メッセージ編集',
message_deleted: 'メッセージ削除',
message_reaction: 'メッセージリアクション',
feedback_received: 'フィードバック受信',
friend_request_received: '友達リクエスト受信',
friend_added: '友達追加',
group_member_joined: 'メンバー参加',
group_member_left: 'メンバー退出',
group_member_banned: 'メンバーBAN',
bot_invited_to_group: 'ボットがグループに招待された',
bot_removed_from_group: 'ボットがグループから削除された',
bot_muted: 'ボットがミュートされた',
bot_unmuted: 'ボットのミュート解除',
platform_specific: 'プラットフォーム固有イベント',
},
routingRules: '条件付きルーティングルール',
routingRulesDescription:
'ルールは順番に評価され、最初に一致したルールのパイプラインにルーティングされます。一致しない場合はデフォルトパイプラインが使用されます。',
+17
View File
@@ -372,6 +372,23 @@ const zhHans = {
eventCustom: '自定义事件',
eventWildcard: '全部事件',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
message_received: '收到消息',
message_edited: '消息被编辑',
message_deleted: '消息被删除',
message_reaction: '消息表态',
feedback_received: '收到反馈',
friend_request_received: '收到好友请求',
friend_added: '好友添加成功',
group_member_joined: '成员加入群组',
group_member_left: '成员离开群组',
group_member_banned: '成员被封禁',
bot_invited_to_group: '机器人被邀入群',
bot_removed_from_group: '机器人被移出群',
bot_muted: '机器人被禁言',
bot_unmuted: '机器人被解除禁言',
platform_specific: '平台特定事件',
},
conditions: '触发条件',
conditionsDescription: '满足所有条件时才触发此绑定,不添加则无条件触发。',
conditionsEmpty: '无条件,始终触发。',