fix(bots): streamline detail layout and event listening

This commit is contained in:
RockChinQ
2026-09-10 15:29:52 +08:00
parent 15fa0f50e3
commit b7a04f7a24
16 changed files with 110 additions and 113 deletions
+29 -53
View File
@@ -4,13 +4,6 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Dialog,
DialogContent,
@@ -36,6 +29,7 @@ import { Bot } from '@/app/infra/entities/api';
import EntityBasicInfoDialog, {
EntityBasicInfoValues,
} from '@/app/home/components/entity-basic-info/EntityBasicInfoDialog';
import AdapterEventDebugDialog from './components/bot-form/AdapterEventDebugDialog';
import EntityTitleEditButton from '@/app/home/components/entity-basic-info/EntityTitleEditButton';
export default function BotDetailContent({ id }: { id: string }) {
@@ -61,6 +55,7 @@ export default function BotDetailContent({ id }: { id: string }) {
}, [id, isCreateMode, bots, setDetailEntityName, t]);
const [activeTab, setActiveTab] = useState('config');
const [adapterLabel, setAdapterLabel] = useState('');
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [basicInfoOpen, setBasicInfoOpen] = useState(false);
const [bot, setBot] = useState<Bot | null>(null);
@@ -191,7 +186,7 @@ export default function BotDetailContent({ id }: { id: string }) {
<>
<div className="flex h-full min-w-0 flex-col">
{/* Sticky Header: title + enable switch + save button */}
<div className="flex items-center justify-between pb-4 shrink-0">
<div className="flex shrink-0 flex-wrap items-center justify-between gap-3 pb-4">
<div className="flex min-w-0 items-center gap-4">
<div className="flex min-w-0 items-center gap-1">
<h1 className="truncate text-xl font-semibold">
@@ -219,14 +214,29 @@ export default function BotDetailContent({ id }: { id: string }) {
)}
</div>
{canManage && (
<Button
type="submit"
form="bot-form"
disabled={!formDirty}
className={activeTab !== 'config' ? 'invisible' : ''}
>
{t('common.save')}
</Button>
<div className="flex shrink-0 items-center gap-2">
<AdapterEventDebugDialog
key={id}
botId={id}
adapterLabel={adapterLabel}
/>
<Button
type="submit"
form="bot-form"
disabled={!formDirty}
className={activeTab !== 'config' ? 'invisible' : ''}
>
{t('common.save')}
</Button>
<Button
type="button"
variant="destructive"
onClick={() => setShowDeleteConfirm(true)}
>
<Trash2 className="size-4" />
{t('common.delete')}
</Button>
</div>
)}
</div>
@@ -286,9 +296,9 @@ export default function BotDetailContent({ id }: { id: string }) {
{/* Tab: Configuration */}
<TabsContent
value="config"
className="mt-4 min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden"
className="mt-4 min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden lg:overflow-hidden"
>
<div className="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-6 pb-8">
<div className="min-h-0 min-w-0 pb-4 lg:h-full lg:pb-0">
<fieldset className="contents" disabled={!canManage}>
<BotForm
ref={botFormRef}
@@ -296,43 +306,9 @@ export default function BotDetailContent({ id }: { id: string }) {
onFormSubmit={handleFormSubmit}
onNewBotCreated={handleNewBotCreated}
onDirtyChange={setFormDirty}
onAdapterLabelChange={setAdapterLabel}
/>
</fieldset>
{/* Card: Danger Zone */}
{canManage && (
<Card className="border-destructive/50">
<CardHeader>
<CardTitle className="text-destructive">
{t('bots.dangerZone')}
</CardTitle>
<CardDescription>
{t('bots.dangerZoneDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium">
{t('bots.deleteBotAction')}
</p>
<p className="text-sm text-muted-foreground">
{t('bots.deleteBotHint')}
</p>
</div>
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => setShowDeleteConfirm(true)}
>
<Trash2 className="size-4 mr-1.5" />
{t('common.delete')}
</Button>
</div>
</CardContent>
</Card>
)}
</div>
</TabsContent>
@@ -192,12 +192,11 @@ export default function AdapterEventDebugDialog({
<Button
type="button"
variant="outline"
size="sm"
disabled={!botId}
onClick={() => setOpen(true)}
title={!botId ? t('bots.adapterEventNeedsSavedBot') : undefined}
>
<RadioTower className="mr-1 h-4 w-4" />
<RadioTower className="size-4" />
{t('bots.adapterEventDebugAction')}
</Button>
@@ -21,8 +21,8 @@ import { systemInfo } from '@/app/infra/http';
import { Agent, Bot } from '@/app/infra/entities/api';
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
import { ExternalLink, ChevronDown, ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import EventBindingsEditor from './EventBindingsEditor';
import AdapterEventDebugDialog from './AdapterEventDebugDialog';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
@@ -101,10 +101,17 @@ interface BotFormProps {
onFormSubmit: (value: z.infer<ReturnType<typeof getFormSchema>>) => void;
onNewBotCreated: (botId: string) => void;
onDirtyChange?: (dirty: boolean) => void;
onAdapterLabelChange?: (label: string) => void;
}
const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
{ initBotId, onFormSubmit, onNewBotCreated, onDirtyChange },
{
initBotId,
onFormSubmit,
onNewBotCreated,
onDirtyChange,
onAdapterLabelChange,
},
ref,
) {
const { t } = useTranslation();
@@ -153,6 +160,12 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
// Watch adapter and adapter_config for filtering
const currentAdapter = form.watch('adapter');
const adapterLabel =
adapterNameList.find((adapter) => adapter.value === currentAdapter)
?.label ?? '';
useEffect(() => {
onAdapterLabelChange?.(adapterLabel);
}, [adapterLabel, onAdapterLabelChange]);
const currentAdapterConfig = form.watch('adapter_config');
// Group adapters by category for the Select dropdown. Legacy adapters are
@@ -440,10 +453,15 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
id="bot-form"
onSubmit={form.handleSubmit(onDynamicFormSubmit)}
aria-busy={isLoading}
className="w-full min-w-0 max-w-full"
className={cn('w-full min-w-0 max-w-full', initBotId && 'lg:h-full')}
>
<fieldset
className="w-full min-w-0 max-w-full space-y-6"
className={cn(
'w-full min-w-0 max-w-full',
initBotId
? 'grid gap-4 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)] lg:grid-rows-[minmax(0,1fr)]'
: 'space-y-6',
)}
disabled={isLoading}
>
{!initBotId && (
@@ -489,14 +507,24 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
)}
{/* Card 2: Adapter Configuration */}
<Card>
<Card
className={cn(
'min-w-0',
initBotId && 'lg:min-h-0 lg:overflow-hidden',
)}
>
<CardHeader>
<CardTitle>{t('bots.adapterConfig')}</CardTitle>
<CardDescription>
{t('bots.adapterConfigDescription')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<CardContent
className={cn(
'min-w-0 space-y-4',
initBotId && 'lg:min-h-0 lg:flex-1 lg:overflow-y-auto',
)}
>
<FormField
control={form.control}
name="adapter"
@@ -507,7 +535,7 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
<span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-2">
<Select
onValueChange={(value) => {
field.onChange(value);
@@ -515,7 +543,7 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
}}
value={field.value}
>
<SelectTrigger className="w-[240px] overflow-hidden">
<SelectTrigger className="w-full min-w-0 overflow-hidden sm:w-[240px]">
{field.value ? (
<div className="flex min-w-0 items-center gap-2">
<img
@@ -693,40 +721,29 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
}}
/>
)}
{currentAdapter && initBotId && (
<div className="flex flex-col gap-3 border-t pt-4 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0">
<p className="text-sm font-medium">
{t('bots.adapterConfigurationTest')}
</p>
<p className="mt-1 text-sm text-muted-foreground">
{t('bots.adapterConfigurationTestDescription')}
</p>
</div>
<AdapterEventDebugDialog
botId={initBotId}
adapterLabel={
adapterNameList.find(
(adapter) => adapter.value === currentAdapter,
)?.label ?? currentAdapter
}
/>
</div>
)}
</CardContent>
</Card>
{/* Card 3: Event Routing */}
{currentAdapter && (
<Card>
<Card
className={cn(
'min-w-0',
initBotId && 'lg:min-h-0 lg:overflow-hidden',
)}
>
<CardHeader>
<CardTitle>{t('bots.eventRouting')}</CardTitle>
<CardDescription>
{t('bots.eventRoutingDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<CardContent
className={cn(
'min-w-0',
initBotId && 'lg:min-h-0 lg:flex-1 lg:overflow-y-auto',
)}
>
<EventBindingsEditor
form={form}
botId={initBotId}
+1 -1
View File
@@ -450,7 +450,7 @@ const enUS = {
routeFallbackIgnored:
'Events that match no route are ignored. Add a catch-all route only when every event needs an explicit outcome.',
testRoute: 'Check route',
adapterEventDebugAction: 'Listen for platform events',
adapterEventDebugAction: 'Test listener',
adapterEventDebugTitle: 'Platform event debugging',
adapterEventDebugDescription:
'Trigger an event in {{platform}}. It will appear here when the adapter receives it.',
+1
View File
@@ -348,6 +348,7 @@ const esES = {
"Uses the Workspace owner's LangBot Account billing and credits.",
},
bots: {
adapterEventDebugAction: 'Probar escucha',
title: 'Bots',
description:
'Crea y gestiona Bots, que son los puntos de entrada para que LangBot se conecte con diversas plataformas',
+1 -1
View File
@@ -458,7 +458,7 @@ const jaJP = {
routeFallbackIgnored:
'どのルートにも一致しないイベントは無視されます。すべてのイベントに明示的な結果が必要な場合のみ、フォールバックを追加してください。',
testRoute: 'ルートを確認',
adapterEventDebugAction: 'プラットフォームイベント監視',
adapterEventDebugAction: 'イベント監視テスト',
adapterEventDebugTitle: 'プラットフォームイベントのデバッグ',
adapterEventDebugDescription:
'{{platform}} でイベントを発生させると、アダプターの受信後にここへ表示されます。',
+1
View File
@@ -346,6 +346,7 @@ const ruRU = {
"Uses the Workspace owner's LangBot Account billing and credits.",
},
bots: {
adapterEventDebugAction: 'Тест прослушивания',
title: 'Боты',
description:
'Создание и управление ботами — точками входа LangBot для подключения к различным платформам',
+1
View File
@@ -333,6 +333,7 @@ const thTH = {
"Uses the Workspace owner's LangBot Account billing and credits.",
},
bots: {
adapterEventDebugAction: 'ทดสอบการรับเหตุการณ์',
title: 'บอท',
description:
'สร้างและจัดการ Bot ซึ่งเป็นจุดเชื่อมต่อของ LangBot กับแพลตฟอร์มต่างๆ',
+1
View File
@@ -342,6 +342,7 @@ const viVN = {
"Uses the Workspace owner's LangBot Account billing and credits.",
},
bots: {
adapterEventDebugAction: 'Kiểm tra lắng nghe',
title: 'Bot',
description:
'Tạo và quản lý Bot, là điểm kết nối của LangBot với các nền tảng khác nhau',
+1 -1
View File
@@ -428,7 +428,7 @@ const zhHans = {
routeFallbackIgnored:
'未命中任何路由的事件会被忽略。只有需要为每个事件指定结果时,才添加全局兜底路由。',
testRoute: '检查路由',
adapterEventDebugAction: '监听平台事件',
adapterEventDebugAction: '测试监听',
adapterEventDebugTitle: '平台事件调试',
adapterEventDebugDescription:
'在 {{platform}} 中触发事件,适配器收到后会显示在这里。',
+1
View File
@@ -322,6 +322,7 @@ const zhHant = {
"Uses the Workspace owner's LangBot Account billing and credits.",
},
bots: {
adapterEventDebugAction: '測試監聽',
title: '機器人',
description: '建立和管理機器人,這是 LangBot 與各個平台連接的入口',
createBot: '建立機器人',