mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-28 13:17:14 +00:00
refactor(agents): use shadcn processor type toggle
This commit is contained in:
@@ -1,16 +1,14 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type React from 'react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Bot, CheckCircle2, Workflow } from 'lucide-react';
|
import { Bot, Workflow } from 'lucide-react';
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import { AgentKind } from '@/app/infra/entities/api';
|
import { AgentKind } from '@/app/infra/entities/api';
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -79,26 +77,18 @@ export default function AgentCreateContent({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeOptions: Array<{
|
const typeOptions = [
|
||||||
kind: AgentKind;
|
|
||||||
icon: React.ElementType;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
badge: string;
|
|
||||||
}> = [
|
|
||||||
{
|
{
|
||||||
kind: 'agent',
|
kind: 'agent' as const,
|
||||||
icon: Bot,
|
icon: Bot,
|
||||||
title: t('agents.agentType'),
|
title: t('agents.agentType'),
|
||||||
description: t('agents.agentTypeDescription'),
|
helper: t('agents.allEvents'),
|
||||||
badge: t('agents.allEvents'),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: 'pipeline',
|
kind: 'pipeline' as const,
|
||||||
icon: Workflow,
|
icon: Workflow,
|
||||||
title: t('agents.pipelineType'),
|
title: t('agents.pipelineType'),
|
||||||
description: t('agents.pipelineTypeDescription'),
|
helper: t('agents.messageEventsOnly'),
|
||||||
badge: t('agents.messageEventsOnly'),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -131,63 +121,41 @@ export default function AgentCreateContent({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-1">
|
<ToggleGroup
|
||||||
|
type="single"
|
||||||
|
value={kind}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (value) handleKindChange(value as AgentKind);
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
spacing={3}
|
||||||
|
className="grid w-full gap-3 sm:grid-cols-2 lg:grid-cols-1"
|
||||||
|
>
|
||||||
{typeOptions.map((option) => {
|
{typeOptions.map((option) => {
|
||||||
const Icon = option.icon;
|
const Icon = option.icon;
|
||||||
const selected = kind === option.kind;
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<ToggleGroupItem
|
||||||
key={option.kind}
|
key={option.kind}
|
||||||
|
value={option.kind}
|
||||||
data-processor-kind={option.kind}
|
data-processor-kind={option.kind}
|
||||||
className={cn(
|
aria-label={`${option.title} ${option.helper}`}
|
||||||
'gap-0 py-0 transition-[border-color,box-shadow,background-color]',
|
className="h-auto min-h-24 w-full justify-start gap-3 rounded-lg border px-4 py-4 text-left shadow-none hover:bg-muted/40 data-[state=on]:border-[#2288ee]/50 data-[state=on]:bg-blue-50/60 data-[state=on]:text-foreground data-[state=on]:shadow-none dark:data-[state=on]:border-blue-500/50 dark:data-[state=on]:bg-blue-500/10"
|
||||||
selected
|
|
||||||
? 'border-primary bg-primary/[0.035] shadow-sm ring-1 ring-primary/20'
|
|
||||||
: 'hover:border-primary/50',
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<CardContent className="h-full p-0">
|
<span className="flex size-9 shrink-0 items-center justify-center rounded-md border bg-background text-[#2288ee] shadow-xs">
|
||||||
<Button
|
<Icon className="size-4" />
|
||||||
type="button"
|
</span>
|
||||||
variant="ghost"
|
<span className="min-w-0 space-y-1">
|
||||||
aria-pressed={selected}
|
<span className="block text-sm font-medium">
|
||||||
onClick={() => handleKindChange(option.kind)}
|
{option.title}
|
||||||
className="h-full min-h-32 w-full items-start justify-start whitespace-normal rounded-xl p-4 text-left hover:bg-transparent"
|
</span>
|
||||||
>
|
<span className="block text-xs font-normal text-muted-foreground">
|
||||||
<span
|
{option.helper}
|
||||||
className={cn(
|
</span>
|
||||||
'flex size-10 shrink-0 items-center justify-center rounded-lg border bg-background',
|
</span>
|
||||||
selected &&
|
</ToggleGroupItem>
|
||||||
'border-primary/30 bg-primary/10 text-primary',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon className="size-5" />
|
|
||||||
</span>
|
|
||||||
<span className="min-w-0 flex-1 space-y-2">
|
|
||||||
<span className="flex items-center justify-between gap-3">
|
|
||||||
<span className="font-semibold">
|
|
||||||
{option.title}
|
|
||||||
</span>
|
|
||||||
{selected && (
|
|
||||||
<CheckCircle2 className="size-4 shrink-0 text-primary" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<Badge
|
|
||||||
variant={selected ? 'default' : 'secondary'}
|
|
||||||
className="font-normal"
|
|
||||||
>
|
|
||||||
{option.badge}
|
|
||||||
</Badge>
|
|
||||||
<span className="block text-sm leading-relaxed text-muted-foreground">
|
|
||||||
{option.description}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</ToggleGroup>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ test.describe('frontend CRUD smoke flows', () => {
|
|||||||
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
|
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
|
||||||
|
|
||||||
await page.goto('/home/agents?id=new');
|
await page.goto('/home/agents?id=new');
|
||||||
await page.getByRole('button', { name: /^Pipeline/ }).click();
|
await page.getByRole('radio', { name: /^Pipeline/ }).click();
|
||||||
await page.locator('input[name="name"]').fill('Viewer Pipeline');
|
await page.locator('input[name="name"]').fill('Viewer Pipeline');
|
||||||
await page
|
await page
|
||||||
.locator('input[name="description"]')
|
.locator('input[name="description"]')
|
||||||
@@ -173,18 +173,20 @@ test.describe('frontend CRUD smoke flows', () => {
|
|||||||
await page.goto('/home/agents?id=new');
|
await page.goto('/home/agents?id=new');
|
||||||
const agentTypeCard = page.locator('[data-processor-kind="agent"]');
|
const agentTypeCard = page.locator('[data-processor-kind="agent"]');
|
||||||
const pipelineTypeCard = page.locator('[data-processor-kind="pipeline"]');
|
const pipelineTypeCard = page.locator('[data-processor-kind="pipeline"]');
|
||||||
await expect(agentTypeCard).toHaveAttribute('data-slot', 'card');
|
await expect(agentTypeCard).toHaveAttribute(
|
||||||
await expect(pipelineTypeCard).toHaveAttribute('data-slot', 'card');
|
'data-slot',
|
||||||
await expect(
|
'toggle-group-item',
|
||||||
agentTypeCard.getByRole('button', { name: /^Agent/ }),
|
);
|
||||||
).toHaveAttribute('aria-pressed', 'true');
|
await expect(pipelineTypeCard).toHaveAttribute(
|
||||||
|
'data-slot',
|
||||||
|
'toggle-group-item',
|
||||||
|
);
|
||||||
|
await expect(agentTypeCard).toHaveAttribute('aria-checked', 'true');
|
||||||
await expect(page.getByTestId('agent-diagram')).toBeVisible();
|
await expect(page.getByTestId('agent-diagram')).toBeVisible();
|
||||||
await expect(page.getByTestId('pipeline-diagram')).toHaveCount(0);
|
await expect(page.getByTestId('pipeline-diagram')).toHaveCount(0);
|
||||||
|
|
||||||
await page.getByRole('button', { name: /^Pipeline/ }).click();
|
await page.getByRole('radio', { name: /^Pipeline/ }).click();
|
||||||
await expect(
|
await expect(pipelineTypeCard).toHaveAttribute('aria-checked', 'true');
|
||||||
pipelineTypeCard.getByRole('button', { name: /^Pipeline/ }),
|
|
||||||
).toHaveAttribute('aria-pressed', 'true');
|
|
||||||
await expect(page.getByTestId('pipeline-diagram')).toBeVisible();
|
await expect(page.getByTestId('pipeline-diagram')).toBeVisible();
|
||||||
await expect(page.getByTestId('agent-diagram')).toHaveCount(0);
|
await expect(page.getByTestId('agent-diagram')).toHaveCount(0);
|
||||||
|
|
||||||
@@ -734,7 +736,7 @@ test.describe('pipeline advanced flows', () => {
|
|||||||
|
|
||||||
// Create a pipeline
|
// Create a pipeline
|
||||||
await page.goto('/home/agents?id=new');
|
await page.goto('/home/agents?id=new');
|
||||||
await page.getByRole('button', { name: /^Pipeline/ }).click();
|
await page.getByRole('radio', { name: /^Pipeline/ }).click();
|
||||||
await page.locator('input[name="name"]').fill('Tab Test Pipeline');
|
await page.locator('input[name="name"]').fill('Tab Test Pipeline');
|
||||||
await submit(page);
|
await submit(page);
|
||||||
|
|
||||||
@@ -765,7 +767,7 @@ test.describe('pipeline advanced flows', () => {
|
|||||||
|
|
||||||
// Create a pipeline
|
// Create a pipeline
|
||||||
await page.goto('/home/agents?id=new');
|
await page.goto('/home/agents?id=new');
|
||||||
await page.getByRole('button', { name: /^Pipeline/ }).click();
|
await page.getByRole('radio', { name: /^Pipeline/ }).click();
|
||||||
await page.locator('input[name="name"]').fill('Dirty Form Pipeline');
|
await page.locator('input[name="name"]').fill('Dirty Form Pipeline');
|
||||||
await submit(page);
|
await submit(page);
|
||||||
|
|
||||||
@@ -787,7 +789,7 @@ test.describe('pipeline advanced flows', () => {
|
|||||||
await installLangBotApiMocks(page, { authenticated: true });
|
await installLangBotApiMocks(page, { authenticated: true });
|
||||||
|
|
||||||
await page.goto('/home/agents?id=new');
|
await page.goto('/home/agents?id=new');
|
||||||
await page.getByRole('button', { name: /^Pipeline/ }).click();
|
await page.getByRole('radio', { name: /^Pipeline/ }).click();
|
||||||
|
|
||||||
// Submit without filling name
|
// Submit without filling name
|
||||||
await submit(page);
|
await submit(page);
|
||||||
@@ -1038,7 +1040,7 @@ test.describe('cross-resource flows', () => {
|
|||||||
|
|
||||||
// Create a pipeline first
|
// Create a pipeline first
|
||||||
await page.goto('/home/agents?id=new');
|
await page.goto('/home/agents?id=new');
|
||||||
await page.getByRole('button', { name: /^Pipeline/ }).click();
|
await page.getByRole('radio', { name: /^Pipeline/ }).click();
|
||||||
await page.locator('input[name="name"]').fill('Production Pipeline');
|
await page.locator('input[name="name"]').fill('Production Pipeline');
|
||||||
await submit(page);
|
await submit(page);
|
||||||
await expect(page).toHaveURL(/\/home\/agents\?id=pipeline-1$/);
|
await expect(page).toHaveURL(/\/home\/agents\?id=pipeline-1$/);
|
||||||
|
|||||||
Reference in New Issue
Block a user