mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-17 07:17:18 +00:00
feat(processors): refine plugin processor configuration and presentation
This commit is contained in:
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { eventPatternLabel } from '@/app/home/components/event-patterns/event-pattern-groups';
|
||||
import { RefreshCw, Trash2, ScrollText } from 'lucide-react';
|
||||
import { RefreshCw, Trash2, ScrollText, Settings2 } from 'lucide-react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { toast } from 'sonner';
|
||||
import type {
|
||||
@@ -26,6 +26,7 @@ import EventProcessorTrace, {
|
||||
} from './components/EventProcessorTrace';
|
||||
import ProcessorRunList from './components/ProcessorRunList';
|
||||
import EventProcessorSettings from './components/EventProcessorSettings';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
|
||||
export default function EventProcessorDetailContent({
|
||||
agent,
|
||||
@@ -47,6 +48,7 @@ export default function EventProcessorDetailContent({
|
||||
onSaved: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('config');
|
||||
const [platformTools, setPlatformTools] = useState<AgentPlatformTool[]>([]);
|
||||
const toolLabels = Object.fromEntries(
|
||||
platformTools.map((tool) => [tool.name, extractI18nObject(tool.label)]),
|
||||
@@ -235,13 +237,11 @@ export default function EventProcessorDetailContent({
|
||||
}, [id, selected, eventCursor, events]);
|
||||
|
||||
async function save() {
|
||||
if (
|
||||
!canManage ||
|
||||
saving ||
|
||||
!component ||
|
||||
!((await validate.current?.()) ?? true)
|
||||
)
|
||||
if (!canManage || saving || !component) return false;
|
||||
if (!((await validate.current?.()) ?? true)) {
|
||||
setActiveTab('config');
|
||||
return false;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await httpClient.updateAgent(id, {
|
||||
@@ -265,9 +265,126 @@ export default function EventProcessorDetailContent({
|
||||
}
|
||||
}
|
||||
|
||||
const logsContent = (
|
||||
<div className="flex h-full min-h-0 flex-col gap-3">
|
||||
<form
|
||||
id="event-processor-form"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void save();
|
||||
}}
|
||||
/>
|
||||
{failed && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
{t('agents.eventProcessor.loadError')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<div className="flex shrink-0 items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium">
|
||||
{t('agents.eventProcessor.runs')}{' '}
|
||||
<span className="text-muted-foreground">({runs.length})</span>
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={t('agents.eventProcessor.refresh')}
|
||||
onClick={() => void refreshLatestRun()}
|
||||
>
|
||||
<RefreshCw className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{runs.length > 0 && (
|
||||
<ProcessorRunList
|
||||
runs={runs}
|
||||
selectedId={selected?.run_id}
|
||||
onSelect={(run) => void openRun(run)}
|
||||
footer={
|
||||
cursor !== null ? (
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="ghost"
|
||||
disabled={pagingRuns}
|
||||
onClick={() => void loadMoreRuns()}
|
||||
>
|
||||
{t('agents.eventProcessor.loadMore')}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<div className="space-y-2 pr-3">
|
||||
{!selected ? (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
{loading
|
||||
? t('common.loading')
|
||||
: t('agents.eventProcessor.noRuns')}
|
||||
<Button asChild variant="link" className="h-auto px-0">
|
||||
<Link to="/home/bots">
|
||||
{t('agents.eventProcessor.bindBot')}
|
||||
</Link>
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<div className="border-b pb-2">
|
||||
<p className="text-sm font-medium">
|
||||
{eventPatternLabel(selected.metadata.event_type ?? '', t)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{new Date(selected.created_at * 1000).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<Badge
|
||||
variant={
|
||||
selected.status === 'failed' ? 'destructive' : 'outline'
|
||||
}
|
||||
>
|
||||
{t(`agents.eventProcessor.status_${selected.status}`, {
|
||||
defaultValue: selected.status,
|
||||
})}
|
||||
</Badge>
|
||||
<ProcessorPayload
|
||||
title={t('agents.eventProcessor.input')}
|
||||
value={selected.metadata.input_event}
|
||||
/>
|
||||
{selected.metadata.delivery != null && (
|
||||
<ProcessorPayload
|
||||
title={t('agents.eventProcessor.destination')}
|
||||
value={selected.metadata.delivery}
|
||||
/>
|
||||
)}
|
||||
<EventProcessorTrace events={events} toolLabels={toolLabels} />
|
||||
{selected.status === 'failed' && selected.status_reason && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription className="break-words">
|
||||
{selected.status_reason}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{eventCursor !== null && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={pagingEvents}
|
||||
onClick={() => void loadMoreEvents()}
|
||||
>
|
||||
{t('agents.eventProcessor.loadMore')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<ProcessorDetailWorkbench
|
||||
title={`${agent.emoji || '⚡'} ${agent.name}`}
|
||||
title={`${agent.emoji || '🧩'} ${agent.name}`}
|
||||
titleAction={
|
||||
canManage ? <EntityTitleEditButton onClick={onEdit} /> : undefined
|
||||
}
|
||||
@@ -275,10 +392,10 @@ export default function EventProcessorDetailContent({
|
||||
<EventProcessorSettings
|
||||
components={components}
|
||||
value={componentRef}
|
||||
parameters={parameters}
|
||||
disabled={!canManage || saving || loading}
|
||||
onChange={(value) => {
|
||||
setComponentRef(value);
|
||||
setActiveTab('config');
|
||||
const descriptor = components.find((item) => item.id === value);
|
||||
setParameters(
|
||||
Object.fromEntries(
|
||||
@@ -289,10 +406,6 @@ export default function EventProcessorDetailContent({
|
||||
);
|
||||
validate.current = null;
|
||||
}}
|
||||
onParametersChange={setParameters}
|
||||
onValidate={(fn) => {
|
||||
validate.current = fn;
|
||||
}}
|
||||
/>
|
||||
}
|
||||
status={
|
||||
@@ -313,127 +426,51 @@ export default function EventProcessorDetailContent({
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
configTitle={t('agents.eventProcessor.trace')}
|
||||
configIcon={<ScrollText className="size-4" />}
|
||||
configContent={
|
||||
<div className="flex h-full min-h-0 flex-col gap-3">
|
||||
<form
|
||||
id="event-processor-form"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void save();
|
||||
}}
|
||||
/>
|
||||
{failed && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
{t('agents.eventProcessor.loadError')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<div className="flex shrink-0 items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium">
|
||||
{t('agents.eventProcessor.runs')}{' '}
|
||||
<span className="text-muted-foreground">({runs.length})</span>
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={t('agents.eventProcessor.refresh')}
|
||||
onClick={() => void refreshLatestRun()}
|
||||
>
|
||||
<RefreshCw className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{runs.length > 0 && (
|
||||
<ProcessorRunList
|
||||
runs={runs}
|
||||
selectedId={selected?.run_id}
|
||||
onSelect={(run) => void openRun(run)}
|
||||
footer={
|
||||
cursor !== null ? (
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="ghost"
|
||||
disabled={pagingRuns}
|
||||
onClick={() => void loadMoreRuns()}
|
||||
>
|
||||
{t('agents.eventProcessor.loadMore')}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<div className="space-y-2 pr-3">
|
||||
{!selected ? (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
{loading
|
||||
? t('common.loading')
|
||||
: t('agents.eventProcessor.noRuns')}
|
||||
<Button asChild variant="link" className="h-auto px-0">
|
||||
<Link to="/home/bots">
|
||||
{t('agents.eventProcessor.bindBot')}
|
||||
</Link>
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<div className="border-b pb-2">
|
||||
<p className="text-sm font-medium">
|
||||
{eventPatternLabel(selected.metadata.event_type ?? '', t)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{new Date(selected.created_at * 1000).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<Badge
|
||||
variant={
|
||||
selected.status === 'failed' ? 'destructive' : 'outline'
|
||||
}
|
||||
>
|
||||
{t(`agents.eventProcessor.status_${selected.status}`, {
|
||||
defaultValue: selected.status,
|
||||
})}
|
||||
</Badge>
|
||||
<ProcessorPayload
|
||||
title={t('agents.eventProcessor.input')}
|
||||
value={selected.metadata.input_event}
|
||||
/>
|
||||
{selected.metadata.delivery != null && (
|
||||
<ProcessorPayload
|
||||
title={t('agents.eventProcessor.destination')}
|
||||
value={selected.metadata.delivery}
|
||||
configTitle={t('agents.eventProcessor.type')}
|
||||
configTabs={{
|
||||
value: activeTab,
|
||||
onValueChange: setActiveTab,
|
||||
items: [
|
||||
{
|
||||
value: 'config',
|
||||
label: t('agents.eventProcessor.configTab'),
|
||||
icon: <Settings2 className="size-4" />,
|
||||
content: (
|
||||
<div className="h-full overflow-y-auto">
|
||||
{!component ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('agents.eventProcessor.selectComponent')}
|
||||
</p>
|
||||
) : component.config_schema.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('agents.eventProcessor.noSettings')}
|
||||
</p>
|
||||
) : (
|
||||
<fieldset disabled={!canManage || saving}>
|
||||
<DynamicFormComponent
|
||||
key={componentRef}
|
||||
itemConfigList={component.config_schema}
|
||||
initialValues={parameters}
|
||||
onSubmit={(values) =>
|
||||
setParameters(values as Record<string, unknown>)
|
||||
}
|
||||
onValidate={(fn) => {
|
||||
validate.current = fn;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<EventProcessorTrace
|
||||
events={events}
|
||||
toolLabels={toolLabels}
|
||||
/>
|
||||
{selected.status === 'failed' && selected.status_reason && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription className="break-words">
|
||||
{selected.status_reason}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{eventCursor !== null && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={pagingEvents}
|
||||
onClick={() => void loadMoreEvents()}
|
||||
>
|
||||
{t('agents.eventProcessor.loadMore')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
}
|
||||
</fieldset>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'logs',
|
||||
label: t('agents.eventProcessor.logsTab'),
|
||||
icon: <ScrollText className="size-4" />,
|
||||
content: logsContent,
|
||||
},
|
||||
],
|
||||
}}
|
||||
debugTitle={canOperate ? t('agents.debugTab') : undefined}
|
||||
debugDescription={t('agents.eventProcessor.debugNotice')}
|
||||
debugContent={
|
||||
@@ -452,6 +489,7 @@ export default function EventProcessorDetailContent({
|
||||
hasUnsavedChanges={dirty}
|
||||
beforeRun={save}
|
||||
onRunFinished={() => {
|
||||
setActiveTab('logs');
|
||||
void refreshLatestRun();
|
||||
}}
|
||||
supportedEventPatterns={component.supported_event_patterns}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { Bot, Workflow, FileCode2 } from 'lucide-react';
|
||||
import { Bot, Workflow, Puzzle } from 'lucide-react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { AgentKind } from '@/app/infra/entities/api';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -52,12 +52,12 @@ export default function AgentCreateContent({
|
||||
|
||||
function handleKindChange(nextKind: AgentKind) {
|
||||
const previousDefaultEmoji =
|
||||
kind === 'pipeline' ? '⚙️' : kind === 'event_processor' ? '⚡' : '🤖';
|
||||
kind === 'pipeline' ? '⚙️' : kind === 'event_processor' ? '🧩' : '🤖';
|
||||
const nextDefaultEmoji =
|
||||
nextKind === 'pipeline'
|
||||
? '⚙️'
|
||||
: nextKind === 'event_processor'
|
||||
? '⚡'
|
||||
? '🧩'
|
||||
: '🤖';
|
||||
setKind(nextKind);
|
||||
const currentEmoji = form.getValues('emoji');
|
||||
@@ -72,7 +72,13 @@ export default function AgentCreateContent({
|
||||
kind,
|
||||
name: values.name,
|
||||
description: values.description ?? '',
|
||||
emoji: values.emoji || (kind === 'pipeline' ? '⚙️' : '🤖'),
|
||||
emoji:
|
||||
values.emoji ||
|
||||
(kind === 'pipeline'
|
||||
? '⚙️'
|
||||
: kind === 'event_processor'
|
||||
? '🧩'
|
||||
: '🤖'),
|
||||
})
|
||||
.then((resp) => {
|
||||
toast.success(t('agents.createSuccess'));
|
||||
@@ -98,7 +104,7 @@ export default function AgentCreateContent({
|
||||
},
|
||||
{
|
||||
kind: 'event_processor' as const,
|
||||
icon: FileCode2,
|
||||
icon: Puzzle,
|
||||
title: t('agents.eventProcessor.type'),
|
||||
description: t('agents.eventProcessor.description'),
|
||||
},
|
||||
@@ -107,13 +113,19 @@ export default function AgentCreateContent({
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||
<h1 className="text-xl font-semibold">{t('agents.create')}</h1>
|
||||
<h1 className="text-xl font-semibold">
|
||||
{t('agents.eventProcessor.createPageTitle')}
|
||||
</h1>
|
||||
<Button
|
||||
type="submit"
|
||||
form="agent-create-form"
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
{t('common.submit')}
|
||||
{t(
|
||||
kind === 'event_processor'
|
||||
? 'agents.eventProcessor.create'
|
||||
: 'common.submit',
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -396,19 +396,15 @@ export default function AgentDebugPanel({
|
||||
</p>
|
||||
</div>
|
||||
{entries.length === 0 ? (
|
||||
<Alert className="my-4 bg-muted/20">
|
||||
<CircleHelp className="size-4" />
|
||||
<AlertTitle>
|
||||
{t(processor ? 'agents.debugTab' : 'agents.debugEmptyTitle')}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t(
|
||||
processor
|
||||
? 'agents.eventProcessor.debugDescription'
|
||||
: 'agents.debugEmptyTranscript',
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
!processor && (
|
||||
<Alert className="my-4 bg-muted/20">
|
||||
<CircleHelp className="size-4" />
|
||||
<AlertTitle>{t('agents.debugEmptyTitle')}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('agents.debugEmptyTranscript')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{entries
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { Settings2, Puzzle } from 'lucide-react';
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from '@/components/ui/popover';
|
||||
import { Puzzle } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { EventProcessorDescriptor } from '@/app/infra/entities/api';
|
||||
@@ -19,7 +13,6 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
|
||||
function ProcessorComponentContent({
|
||||
component,
|
||||
@@ -72,48 +65,25 @@ function ProcessorComponentContent({
|
||||
export default function EventProcessorSettings({
|
||||
components,
|
||||
value,
|
||||
parameters,
|
||||
onChange,
|
||||
onParametersChange,
|
||||
onValidate,
|
||||
disabled = false,
|
||||
}: {
|
||||
components: EventProcessorDescriptor[];
|
||||
value: string;
|
||||
parameters: Record<string, unknown>;
|
||||
onChange: (value: string) => void;
|
||||
onParametersChange: (value: Record<string, unknown>) => void;
|
||||
onValidate?: (validate: () => Promise<boolean>) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const selected = components.find((item) => item.id === value);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
return (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Label className="sr-only" htmlFor="event-processor-component">
|
||||
{t('agents.eventProcessor.component')}
|
||||
</Label>
|
||||
<Select
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onValueChange={(next) => {
|
||||
onChange(next);
|
||||
const component = components.find((item) => item.id === next);
|
||||
setSettingsOpen(
|
||||
Boolean(
|
||||
component?.config_schema.some(
|
||||
(field) =>
|
||||
field.required &&
|
||||
(field.default == null || field.default === ''),
|
||||
),
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Select value={value} disabled={disabled} onValueChange={onChange}>
|
||||
<SelectTrigger
|
||||
id="event-processor-component"
|
||||
className="w-[22rem] max-w-[calc(100vw-8rem)] bg-[#ffffff] dark:bg-[#2a2a2e]"
|
||||
className="w-[13.2rem] max-w-[calc(100vw-8rem)] bg-[#ffffff] dark:bg-[#2a2a2e]"
|
||||
>
|
||||
{selected ? (
|
||||
<ProcessorComponentContent component={selected} />
|
||||
@@ -153,44 +123,6 @@ export default function EventProcessorSettings({
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selected && selected.config_schema.length > 0 && (
|
||||
<Popover open={settingsOpen} onOpenChange={setSettingsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={disabled}
|
||||
aria-label={t('agents.eventProcessor.pluginSettings')}
|
||||
title={t('agents.eventProcessor.pluginSettings')}
|
||||
>
|
||||
<Settings2 className="size-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="max-h-[70vh] overflow-y-auto space-y-3"
|
||||
>
|
||||
<p className="text-sm font-medium">
|
||||
{t('agents.eventProcessor.pluginSettings')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('agents.eventProcessor.pluginSettingsDescription')}
|
||||
</p>
|
||||
<fieldset disabled={disabled}>
|
||||
<DynamicFormComponent
|
||||
key={value}
|
||||
itemConfigList={selected.config_schema}
|
||||
initialValues={parameters}
|
||||
onSubmit={(values) =>
|
||||
onParametersChange(values as Record<string, unknown>)
|
||||
}
|
||||
onValidate={onValidate}
|
||||
/>
|
||||
</fieldset>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -373,18 +373,38 @@ function PipelineDiagram() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProcessorTypeDiagram({ kind }: { kind: AgentKind }) {
|
||||
function EventProcessorDiagram() {
|
||||
const { t } = useTranslation();
|
||||
if (kind === 'event_processor')
|
||||
return (
|
||||
<div className="flex h-full flex-col justify-center gap-6 rounded-xl border bg-muted/20 p-8">
|
||||
const codeLines = [
|
||||
<>
|
||||
<span className="text-[#8b5cf6]">@handler</span>
|
||||
</>,
|
||||
<>
|
||||
<span className="text-[#2288ee]">async def</span>{' '}
|
||||
<span className="text-[#19b8c9]">on_event</span>(event):
|
||||
</>,
|
||||
<>
|
||||
<span className="text-[#2288ee]">if</span> event.type =={' '}
|
||||
<span className="text-amber-500">"message"</span>:
|
||||
</>,
|
||||
<>
|
||||
<span className="text-[#2288ee]">await</span> event.process()
|
||||
</>,
|
||||
<>
|
||||
<span className="text-muted-foreground"># output → trace</span>
|
||||
</>,
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="processor-diagram grid h-full w-full overflow-hidden bg-muted/20 lg:grid-cols-[minmax(0,1.05fr)_minmax(280px,0.95fr)]"
|
||||
data-testid="event-processor-diagram"
|
||||
>
|
||||
<div className="flex min-w-0 flex-col justify-center gap-5 p-8 lg:p-10">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{t('agents.eventProcessor.type')}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('agents.eventProcessor.description')}
|
||||
</p>
|
||||
{['input', 'component', 'trace'].map((step, index) => (
|
||||
{['input', 'processWithPlugin', 'trace'].map((step, index) => (
|
||||
<div
|
||||
key={step}
|
||||
className="flex items-center gap-3 rounded-lg border bg-background p-4"
|
||||
@@ -393,10 +413,52 @@ export default function ProcessorTypeDiagram({ kind }: { kind: AgentKind }) {
|
||||
<span>{t(`agents.eventProcessor.${step}`)}</span>
|
||||
</div>
|
||||
))}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('agents.eventProcessor.activation')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
<div
|
||||
className="relative hidden min-w-0 items-center border-l bg-background/45 p-8 lg:flex"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="absolute left-0 top-1/2 w-8 -translate-y-1/2 border-t border-dashed border-[#2288ee]/25" />
|
||||
|
||||
<div className="relative w-full rounded-xl border bg-card">
|
||||
<div className="flex h-11 items-center gap-2 border-b px-4">
|
||||
<span className="font-mono text-[11px] text-muted-foreground">
|
||||
processor.py
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 px-4 py-5 font-mono text-[12px] leading-5">
|
||||
{codeLines.map((line, index) => (
|
||||
<div key={index} className="flex min-w-0 gap-3">
|
||||
<span className="w-4 shrink-0 select-none text-right text-muted-foreground/45">
|
||||
{index + 1}
|
||||
</span>
|
||||
<code
|
||||
className={
|
||||
index === 2 || index === 3
|
||||
? 'min-w-0 pl-3 text-foreground/80'
|
||||
: 'min-w-0 text-foreground/80'
|
||||
}
|
||||
>
|
||||
{line}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mx-4 mb-4 border-t pt-3">
|
||||
<span className="font-mono text-[11px] text-muted-foreground">
|
||||
event → plugin → log
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProcessorTypeDiagram({ kind }: { kind: AgentKind }) {
|
||||
if (kind === 'event_processor') return <EventProcessorDiagram />;
|
||||
return kind === 'agent' ? <AgentDiagram /> : <PipelineDiagram />;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,17 @@ interface ProcessorDetailWorkbenchProps {
|
||||
isSaving: boolean;
|
||||
configTitle: string;
|
||||
configIcon?: ReactNode;
|
||||
configContent: ReactNode;
|
||||
configContent?: ReactNode;
|
||||
configTabs?: {
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
items: {
|
||||
value: string;
|
||||
label: string;
|
||||
icon?: ReactNode;
|
||||
content: ReactNode;
|
||||
}[];
|
||||
};
|
||||
debugTitle?: string;
|
||||
debugDescription?: string;
|
||||
debugContent?: ReactNode;
|
||||
@@ -63,6 +73,7 @@ export default function ProcessorDetailWorkbench({
|
||||
configTitle,
|
||||
configIcon,
|
||||
configContent,
|
||||
configTabs,
|
||||
debugTitle,
|
||||
debugDescription,
|
||||
debugContent,
|
||||
@@ -77,6 +88,52 @@ export default function ProcessorDetailWorkbench({
|
||||
);
|
||||
const hasDebug = Boolean(debugTitle && debugContent);
|
||||
|
||||
const configPanel = (
|
||||
<Card
|
||||
role="region"
|
||||
aria-label={configTitle}
|
||||
className="h-full min-h-[36rem] min-w-0 gap-0 overflow-hidden py-0 lg:min-h-0"
|
||||
>
|
||||
<CardHeader className="flex h-12 shrink-0 flex-row items-center gap-2 border-b px-4 font-medium [.border-b]:pb-0">
|
||||
{configTabs ? (
|
||||
<TabsList aria-label={configTitle}>
|
||||
{configTabs.items.map((tab) => (
|
||||
<TabsTrigger key={tab.value} value={tab.value}>
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
) : (
|
||||
<>
|
||||
{configIcon ?? <Settings className="size-4" />}
|
||||
<span className="truncate">{configTitle}</span>
|
||||
</>
|
||||
)}
|
||||
{isDirty && (
|
||||
<span className="ml-auto flex items-center gap-1.5 text-xs text-amber-600 dark:text-amber-400">
|
||||
<span className="size-1.5 rounded-full bg-amber-500" />
|
||||
{unsavedLabel}
|
||||
</span>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="min-h-0 min-w-0 flex-1 overflow-hidden p-4">
|
||||
{configTabs
|
||||
? configTabs.items.map((tab) => (
|
||||
<TabsContent
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
forceMount
|
||||
className="m-0 h-full min-h-0 data-[state=inactive]:hidden"
|
||||
>
|
||||
{tab.content}
|
||||
</TabsContent>
|
||||
))
|
||||
: configContent}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
value={activeView}
|
||||
@@ -246,25 +303,17 @@ export default function ProcessorDetailWorkbench({
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card
|
||||
role="region"
|
||||
aria-label={configTitle}
|
||||
className="min-h-[36rem] min-w-0 gap-0 overflow-hidden py-0 lg:min-h-0"
|
||||
>
|
||||
<CardHeader className="flex h-12 shrink-0 flex-row items-center gap-2 border-b px-4 font-medium [.border-b]:pb-0">
|
||||
{configIcon ?? <Settings className="size-4" />}
|
||||
<span className="truncate">{configTitle}</span>
|
||||
{isDirty && (
|
||||
<span className="ml-auto flex items-center gap-1.5 text-xs text-amber-600 dark:text-amber-400">
|
||||
<span className="size-1.5 rounded-full bg-amber-500" />
|
||||
{unsavedLabel}
|
||||
</span>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="min-h-0 min-w-0 flex-1 overflow-hidden p-4">
|
||||
{configContent}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{configTabs ? (
|
||||
<Tabs
|
||||
value={configTabs.value}
|
||||
onValueChange={configTabs.onValueChange}
|
||||
className="min-h-0 min-w-0"
|
||||
>
|
||||
{configPanel}
|
||||
</Tabs>
|
||||
) : (
|
||||
configPanel
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
Reference in New Issue
Block a user