refactor(agents): use shadcn processor type toggle

This commit is contained in:
RockChinQ
2026-08-27 14:55:59 +08:00
parent 139494cd63
commit a9b24e59ec
2 changed files with 50 additions and 80 deletions
@@ -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>
+16 -14
View File
@@ -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$/);