mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-26 12:17:14 +00:00
refactor(web): use shadcn processor navigation
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
|||||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
@@ -430,40 +431,26 @@ export default function AgentFormComponent({
|
|||||||
className="mb-2 flex h-full min-h-0 min-w-0 flex-1 flex-col"
|
className="mb-2 flex h-full min-h-0 min-w-0 flex-1 flex-col"
|
||||||
>
|
>
|
||||||
<nav className="mb-4 shrink-0 space-y-2 border-b pb-4">
|
<nav className="mb-4 shrink-0 space-y-2 border-b pb-4">
|
||||||
<div className="overflow-x-auto">
|
<Tabs
|
||||||
<ol className="grid min-w-[44rem] grid-cols-4 gap-2">
|
value={activeSection}
|
||||||
{primarySections.map((section, index) => {
|
onValueChange={(value) =>
|
||||||
const Icon = section.icon;
|
setActiveSection(value as AgentConfigSection)
|
||||||
return (
|
}
|
||||||
<li key={section.name} className="min-w-0">
|
>
|
||||||
<button
|
<div className="overflow-x-auto">
|
||||||
type="button"
|
<TabsList className="grid min-w-[44rem] w-full grid-cols-4">
|
||||||
onClick={() => setActiveSection(section.name)}
|
{primarySections.map((section) => {
|
||||||
className={`flex w-full min-w-0 items-center gap-2 rounded-lg border px-3 py-3 text-left text-sm font-medium transition-colors ${
|
const Icon = section.icon;
|
||||||
activeSection === section.name
|
return (
|
||||||
? 'border-primary/50 bg-primary/5 text-foreground shadow-sm'
|
<TabsTrigger key={section.name} value={section.name}>
|
||||||
: 'border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground'
|
<Icon />
|
||||||
}`}
|
{section.label}
|
||||||
>
|
</TabsTrigger>
|
||||||
<span
|
);
|
||||||
className={`flex size-7 shrink-0 items-center justify-center rounded-full text-xs ${
|
})}
|
||||||
activeSection === section.name
|
</TabsList>
|
||||||
? 'bg-primary text-primary-foreground'
|
</div>
|
||||||
: 'bg-muted text-muted-foreground'
|
</Tabs>
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{index + 1}
|
|
||||||
</span>
|
|
||||||
<Icon className="hidden size-4 shrink-0 xl:block" />
|
|
||||||
<span className="min-w-0 leading-tight">
|
|
||||||
{section.label}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden">
|
<div className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ReactNode, useState } from 'react';
|
import { ReactNode, useState } from 'react';
|
||||||
import { BarChart3, Bug, Settings } from 'lucide-react';
|
import { BarChart3, Bug, Settings } from 'lucide-react';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -69,12 +70,13 @@ export default function ProcessorDetailWorkbench({
|
|||||||
{status && (
|
{status && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span
|
<Badge
|
||||||
|
variant="outline"
|
||||||
role="status"
|
role="status"
|
||||||
aria-label={status.label}
|
aria-label={status.label}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex shrink-0 items-center gap-1.5 rounded-full border px-2 py-1 text-xs font-medium',
|
'rounded-full',
|
||||||
status.tone === 'success' &&
|
status.tone === 'success' &&
|
||||||
'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300',
|
'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300',
|
||||||
status.tone === 'warning' &&
|
status.tone === 'warning' &&
|
||||||
@@ -96,7 +98,7 @@ export default function ProcessorDetailWorkbench({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{status.label}
|
{status.label}
|
||||||
</span>
|
</Badge>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom" className="max-w-72">
|
<TooltipContent side="bottom" className="max-w-72">
|
||||||
<p className="font-medium">{status.label}</p>
|
<p className="font-medium">{status.label}</p>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||||
import { getDefaultValues } from '@/app/home/components/dynamic-form/DynamicFormItemConfig';
|
import { getDefaultValues } from '@/app/home/components/dynamic-form/DynamicFormItemConfig';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
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';
|
||||||
@@ -31,7 +32,6 @@ import {
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -580,64 +580,45 @@ export default function PipelineFormComponent({
|
|||||||
{/* Keep the primary pipeline flow visible while editing. */}
|
{/* Keep the primary pipeline flow visible while editing. */}
|
||||||
{formLabelList.length > 1 && (
|
{formLabelList.length > 1 && (
|
||||||
<nav className="mb-4 shrink-0 space-y-2 border-b pb-4">
|
<nav className="mb-4 shrink-0 space-y-2 border-b pb-4">
|
||||||
<div className="overflow-x-auto">
|
<Tabs value={activeSection} onValueChange={setActiveSection}>
|
||||||
<ol className="grid min-w-[34rem] grid-cols-3 gap-2">
|
<div className="overflow-x-auto">
|
||||||
{primarySections.map((section, index) => {
|
<TabsList className="grid min-w-[34rem] w-full grid-cols-3">
|
||||||
|
{primarySections.map((section) => {
|
||||||
|
const Icon = section.icon;
|
||||||
|
return (
|
||||||
|
<TabsTrigger
|
||||||
|
key={section.name}
|
||||||
|
value={section.name}
|
||||||
|
>
|
||||||
|
<Icon />
|
||||||
|
{section.label}
|
||||||
|
</TabsTrigger>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{secondarySections.map((section) => {
|
||||||
const Icon = section.icon;
|
const Icon = section.icon;
|
||||||
return (
|
return (
|
||||||
<li key={section.name} className="relative min-w-0">
|
<Button
|
||||||
<button
|
key={section.name}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setActiveSection(section.name)}
|
variant={
|
||||||
className={cn(
|
activeSection === section.name
|
||||||
'flex w-full min-w-0 items-center gap-2 rounded-lg border px-3 py-3 text-left text-sm font-medium transition-colors',
|
? 'secondary'
|
||||||
activeSection === section.name
|
: 'ghost'
|
||||||
? 'border-primary/50 bg-primary/5 text-foreground shadow-sm'
|
}
|
||||||
: 'border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground',
|
size="sm"
|
||||||
)}
|
onClick={() => setActiveSection(section.name)}
|
||||||
>
|
>
|
||||||
<span
|
<Icon />
|
||||||
className={cn(
|
{section.label}
|
||||||
'flex size-7 shrink-0 items-center justify-center rounded-full text-xs',
|
</Button>
|
||||||
activeSection === section.name
|
|
||||||
? 'bg-primary text-primary-foreground'
|
|
||||||
: 'bg-muted text-muted-foreground',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{index + 1}
|
|
||||||
</span>
|
|
||||||
<Icon className="hidden size-4 shrink-0 xl:block" />
|
|
||||||
<span className="min-w-0 leading-tight">
|
|
||||||
{section.label}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ol>
|
</div>
|
||||||
</div>
|
</Tabs>
|
||||||
<ul className="flex flex-wrap gap-1">
|
|
||||||
{secondarySections.map((section) => {
|
|
||||||
const Icon = section.icon;
|
|
||||||
return (
|
|
||||||
<li key={section.name}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setActiveSection(section.name)}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium transition-colors',
|
|
||||||
activeSection === section.name
|
|
||||||
? 'bg-accent text-accent-foreground'
|
|
||||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon className="size-3.5" />
|
|
||||||
{section.label}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -33,15 +33,15 @@ test.describe('processor detail workbench', () => {
|
|||||||
.toBe(0);
|
.toBe(0);
|
||||||
expect(debugBox!.y).toBeGreaterThanOrEqual(0);
|
expect(debugBox!.y).toBeGreaterThanOrEqual(0);
|
||||||
|
|
||||||
const flow = configPanel.locator('ol');
|
const flow = configPanel.getByRole('tablist');
|
||||||
await expect(flow.getByRole('button').nth(0)).toContainText(
|
await expect(flow.getByRole('tab').nth(0)).toContainText(
|
||||||
'Basic Information',
|
'Basic Information',
|
||||||
);
|
);
|
||||||
await expect(flow.getByRole('button').nth(1)).toContainText(
|
await expect(flow.getByRole('tab').nth(1)).toContainText(
|
||||||
'Bindable Event Range',
|
'Bindable Event Range',
|
||||||
);
|
);
|
||||||
await expect(flow.getByRole('button').nth(2)).toContainText('Runner');
|
await expect(flow.getByRole('tab').nth(2)).toContainText('Runner');
|
||||||
await expect(flow.getByRole('button').nth(3)).toContainText('Local Agent');
|
await expect(flow.getByRole('tab').nth(3)).toContainText('Local Agent');
|
||||||
|
|
||||||
await expect(configPanel.getByLabel('Name')).toBeVisible();
|
await expect(configPanel.getByLabel('Name')).toBeVisible();
|
||||||
await expect(configPanel.getByLabel('Icon')).toBeVisible();
|
await expect(configPanel.getByLabel('Icon')).toBeVisible();
|
||||||
@@ -56,11 +56,11 @@ test.describe('processor detail workbench', () => {
|
|||||||
),
|
),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await flow.getByRole('button').nth(1).click();
|
await flow.getByRole('tab').nth(1).click();
|
||||||
await expect(
|
await expect(
|
||||||
configPanel.getByText('Bindable Event Range', { exact: true }).last(),
|
configPanel.getByText('Bindable Event Range', { exact: true }).last(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await flow.getByRole('button').nth(3).click();
|
await flow.getByRole('tab').nth(3).click();
|
||||||
await expect(
|
await expect(
|
||||||
configPanel.getByText('Local Agent', { exact: true }).last(),
|
configPanel.getByText('Local Agent', { exact: true }).last(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@@ -91,18 +91,16 @@ test.describe('processor detail workbench', () => {
|
|||||||
.toBe(0);
|
.toBe(0);
|
||||||
expect(debugBox!.y).toBeGreaterThanOrEqual(0);
|
expect(debugBox!.y).toBeGreaterThanOrEqual(0);
|
||||||
|
|
||||||
const flow = configPanel.locator('ol');
|
const flow = configPanel.getByRole('tablist');
|
||||||
await expect(flow.getByRole('button').nth(0)).toContainText(
|
await expect(flow.getByRole('tab').nth(0)).toContainText(
|
||||||
'Trigger Conditions',
|
'Trigger Conditions',
|
||||||
);
|
);
|
||||||
await expect(flow.getByRole('button').nth(1)).toContainText(
|
await expect(flow.getByRole('tab').nth(1)).toContainText('AI Capabilities');
|
||||||
'AI Capabilities',
|
await expect(flow.getByRole('tab').nth(2)).toContainText(
|
||||||
);
|
|
||||||
await expect(flow.getByRole('button').nth(2)).toContainText(
|
|
||||||
'Output Processing',
|
'Output Processing',
|
||||||
);
|
);
|
||||||
|
|
||||||
await flow.getByRole('button').nth(1).click();
|
await flow.getByRole('tab').nth(1).click();
|
||||||
await expect(
|
await expect(
|
||||||
configPanel.getByText('Runtime', { exact: true }).last(),
|
configPanel.getByText('Runtime', { exact: true }).last(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ test('processor forms expose their primary orchestration flow horizontally', ()
|
|||||||
pipelineForm,
|
pipelineForm,
|
||||||
/const primarySectionNames = \['trigger', 'ai', 'output'\]/,
|
/const primarySectionNames = \['trigger', 'ai', 'output'\]/,
|
||||||
);
|
);
|
||||||
assert.match(agentForm, /grid min-w-\[44rem\] grid-cols-4/);
|
assert.match(agentForm, /<TabsList[^>]*grid-cols-4/);
|
||||||
assert.match(pipelineForm, /grid min-w-\[34rem\] grid-cols-3/);
|
assert.match(pipelineForm, /<TabsList[^>]*grid-cols-3/);
|
||||||
|
assert.doesNotMatch(agentForm, /<ol className=/);
|
||||||
|
assert.doesNotMatch(pipelineForm, /<ol className=/);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user