mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-26 20:27:13 +00:00
refactor(bots): group event route creation menu
This commit is contained in:
@@ -1,6 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import {
|
||||||
|
Fragment,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import { UseFormReturn } from 'react-hook-form';
|
import { UseFormReturn } from 'react-hook-form';
|
||||||
@@ -72,7 +79,11 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import {
|
import {
|
||||||
@@ -1543,6 +1554,14 @@ export default function EventBindingsEditor({
|
|||||||
),
|
),
|
||||||
[dryRunEventOptions],
|
[dryRunEventOptions],
|
||||||
);
|
);
|
||||||
|
const otherEventGroups = useMemo(() => {
|
||||||
|
const commonEventTypes = new Set(
|
||||||
|
behaviorPresets.map((preset) => preset.eventType),
|
||||||
|
);
|
||||||
|
return groupEventPatterns(
|
||||||
|
eventOptions.filter((event) => !commonEventTypes.has(event)),
|
||||||
|
);
|
||||||
|
}, [behaviorPresets, eventOptions]);
|
||||||
|
|
||||||
const refreshRouteStatuses = useCallback(async () => {
|
const refreshRouteStatuses = useCallback(async () => {
|
||||||
if (!botId) {
|
if (!botId) {
|
||||||
@@ -1780,6 +1799,9 @@ export default function EventBindingsEditor({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="w-[300px] max-w-[90vw]">
|
<DropdownMenuContent align="start" className="w-[300px] max-w-[90vw]">
|
||||||
|
<DropdownMenuLabel className="px-2 pb-1 pt-1.5 text-xs font-normal text-muted-foreground">
|
||||||
|
{t('bots.commonScenarios')}
|
||||||
|
</DropdownMenuLabel>
|
||||||
{behaviorPresets.map((preset) => {
|
{behaviorPresets.map((preset) => {
|
||||||
const Icon = preset.icon;
|
const Icon = preset.icon;
|
||||||
return (
|
return (
|
||||||
@@ -1799,18 +1821,44 @@ export default function EventBindingsEditor({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuSub>
|
||||||
className="items-start gap-2 py-2"
|
<DropdownMenuSubTrigger
|
||||||
onClick={() => addBinding(dryRunEventOptions[0])}
|
className="items-start gap-2 py-2"
|
||||||
>
|
disabled={otherEventGroups.length === 0}
|
||||||
<Workflow className="mt-0.5 h-4 w-4 shrink-0" />
|
>
|
||||||
<span className="flex min-w-0 flex-col gap-0.5">
|
<Workflow className="mt-0.5 h-4 w-4 shrink-0" />
|
||||||
<span>{t('bots.behaviorCustom')}</span>
|
<span className="flex min-w-0 flex-col gap-0.5 pr-2">
|
||||||
<span className="text-xs text-muted-foreground">
|
<span>{t('bots.behaviorCustom')}</span>
|
||||||
{t('bots.behaviorCustomDescription')}
|
<span className="text-xs font-normal text-muted-foreground">
|
||||||
|
{t('bots.behaviorCustomDescription')}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</DropdownMenuSubTrigger>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuSubContent className="max-h-[min(70vh,32rem)] w-[320px] max-w-[90vw] overflow-y-auto">
|
||||||
|
{otherEventGroups.map((group, groupIndex) => (
|
||||||
|
<Fragment key={group.namespace}>
|
||||||
|
{groupIndex > 0 && <DropdownMenuSeparator />}
|
||||||
|
<DropdownMenuLabel className="px-2 py-1 text-xs font-normal text-muted-foreground">
|
||||||
|
{eventGroupLabel(group.namespace, t)}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{group.patterns.map((event) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={event}
|
||||||
|
className="items-start py-2"
|
||||||
|
onClick={() => addBinding(event)}
|
||||||
|
>
|
||||||
|
<span className="flex min-w-0 flex-col gap-0.5">
|
||||||
|
<span>{eventLabel(event, t)}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{eventDescription(event, t)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuSub>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<RouteDryRunDialog
|
<RouteDryRunDialog
|
||||||
|
|||||||
@@ -392,6 +392,7 @@ const enUS = {
|
|||||||
eventBindings: 'Event Routes',
|
eventBindings: 'Event Routes',
|
||||||
addEventBinding: 'Add Route',
|
addEventBinding: 'Add Route',
|
||||||
addBehavior: 'Add behavior',
|
addBehavior: 'Add behavior',
|
||||||
|
commonScenarios: 'Common scenarios',
|
||||||
dragEventRoute: 'Drag route {{index}}',
|
dragEventRoute: 'Drag route {{index}}',
|
||||||
behaviorReplyMessages: 'Reply to messages',
|
behaviorReplyMessages: 'Reply to messages',
|
||||||
behaviorReplyMessagesDescription:
|
behaviorReplyMessagesDescription:
|
||||||
|
|||||||
@@ -398,6 +398,7 @@ const jaJP = {
|
|||||||
eventBindings: 'イベントルート',
|
eventBindings: 'イベントルート',
|
||||||
addEventBinding: 'ルートを追加',
|
addEventBinding: 'ルートを追加',
|
||||||
addBehavior: '動作を追加',
|
addBehavior: '動作を追加',
|
||||||
|
commonScenarios: 'よく使うシーン',
|
||||||
dragEventRoute: 'ルート {{index}} をドラッグ',
|
dragEventRoute: 'ルート {{index}} をドラッグ',
|
||||||
behaviorReplyMessages: '受信メッセージに返信',
|
behaviorReplyMessages: '受信メッセージに返信',
|
||||||
behaviorReplyMessagesDescription:
|
behaviorReplyMessagesDescription:
|
||||||
|
|||||||
@@ -375,6 +375,7 @@ const zhHans = {
|
|||||||
eventBindings: '事件路由',
|
eventBindings: '事件路由',
|
||||||
addEventBinding: '添加路由',
|
addEventBinding: '添加路由',
|
||||||
addBehavior: '添加行为',
|
addBehavior: '添加行为',
|
||||||
|
commonScenarios: '常用场景',
|
||||||
dragEventRoute: '拖动第 {{index}} 条路由',
|
dragEventRoute: '拖动第 {{index}} 条路由',
|
||||||
behaviorReplyMessages: '回复收到的消息',
|
behaviorReplyMessages: '回复收到的消息',
|
||||||
behaviorReplyMessagesDescription:
|
behaviorReplyMessagesDescription:
|
||||||
|
|||||||
@@ -872,7 +872,7 @@ test.describe('agent and pipeline save concurrency', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe('cross-resource flows', () => {
|
test.describe('cross-resource flows', () => {
|
||||||
test('reorders bot event routes with a visible drag preview', async ({
|
test('adds custom bot events and reorders routes with a drag preview', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await installLangBotApiMocks(page, {
|
await installLangBotApiMocks(page, {
|
||||||
@@ -887,6 +887,9 @@ test.describe('cross-resource flows', () => {
|
|||||||
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
|
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Add behavior' }).click();
|
await page.getByRole('button', { name: 'Add behavior' }).click();
|
||||||
|
await expect(
|
||||||
|
page.getByText('Common scenarios', { exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: /^Reply to messages/ }).click();
|
await page.getByRole('menuitem', { name: /^Reply to messages/ }).click();
|
||||||
await page.getByRole('button', { name: 'Add behavior' }).click();
|
await page.getByRole('button', { name: 'Add behavior' }).click();
|
||||||
await page.getByRole('menuitem', { name: /^Welcome new members/ }).click();
|
await page.getByRole('menuitem', { name: /^Welcome new members/ }).click();
|
||||||
@@ -896,6 +899,25 @@ test.describe('cross-resource flows', () => {
|
|||||||
await expect(routeCards.nth(0)).toContainText('Message received');
|
await expect(routeCards.nth(0)).toContainText('Message received');
|
||||||
await expect(routeCards.nth(1)).toContainText('Member joined group');
|
await expect(routeCards.nth(1)).toContainText('Member joined group');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Add behavior' }).click();
|
||||||
|
await page
|
||||||
|
.getByRole('menuitem', { name: /^Configure another event/ })
|
||||||
|
.hover();
|
||||||
|
const eventSubmenu = page.locator(
|
||||||
|
'[data-slot="dropdown-menu-sub-content"]',
|
||||||
|
);
|
||||||
|
await expect(
|
||||||
|
eventSubmenu.getByText('Messages', { exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
eventSubmenu.getByRole('menuitem', { name: /^Message edited/ }),
|
||||||
|
).toBeVisible();
|
||||||
|
await eventSubmenu
|
||||||
|
.getByRole('menuitem', { name: /^Message edited/ })
|
||||||
|
.click();
|
||||||
|
await expect(routeCards).toHaveCount(3);
|
||||||
|
await expect(routeCards.nth(2)).toContainText('Message edited');
|
||||||
|
|
||||||
const firstHandle = page.getByRole('button', { name: 'Drag route 1' });
|
const firstHandle = page.getByRole('button', { name: 'Drag route 1' });
|
||||||
const secondCard = routeCards.nth(1);
|
const secondCard = routeCards.nth(1);
|
||||||
const handleBox = await firstHandle.boundingBox();
|
const handleBox = await firstHandle.boundingBox();
|
||||||
@@ -930,6 +952,7 @@ test.describe('cross-resource flows', () => {
|
|||||||
const savedRouteCards = page.locator('[data-testid^="event-route-"]');
|
const savedRouteCards = page.locator('[data-testid^="event-route-"]');
|
||||||
await expect(savedRouteCards.nth(0)).toContainText('Member joined group');
|
await expect(savedRouteCards.nth(0)).toContainText('Member joined group');
|
||||||
await expect(savedRouteCards.nth(1)).toContainText('Message received');
|
await expect(savedRouteCards.nth(1)).toContainText('Message received');
|
||||||
|
await expect(savedRouteCards.nth(2)).toContainText('Message edited');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('creates a pipeline then binds it to a bot', async ({ page }) => {
|
test('creates a pipeline then binds it to a bot', async ({ page }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user