mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-08 18:47:14 +00:00
feat(agent-debug): edit event data with compact forms
This commit is contained in:
@@ -1,4 +1,11 @@
|
|||||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
@@ -40,6 +47,13 @@ import {
|
|||||||
} from '@/app/home/components/event-patterns/event-pattern-groups';
|
} from '@/app/home/components/event-patterns/event-pattern-groups';
|
||||||
import EventSelectOptionContent from '@/app/home/components/event-patterns/EventSelectOptionContent';
|
import EventSelectOptionContent from '@/app/home/components/event-patterns/EventSelectOptionContent';
|
||||||
import AgentExecutionTrace from './AgentExecutionTrace';
|
import AgentExecutionTrace from './AgentExecutionTrace';
|
||||||
|
import AgentEventDataEditor from './AgentEventDataEditor';
|
||||||
|
import {
|
||||||
|
createDebugEventData,
|
||||||
|
debugEventInputText,
|
||||||
|
invalidDebugEventField,
|
||||||
|
parseDebugEventData,
|
||||||
|
} from './debug-event-data';
|
||||||
import { executionSteps, type DebugExecutionEvent } from './debug-execution';
|
import { executionSteps, type DebugExecutionEvent } from './debug-execution';
|
||||||
|
|
||||||
interface AgentDebugPanelProps {
|
interface AgentDebugPanelProps {
|
||||||
@@ -63,104 +77,6 @@ interface DebugEntry {
|
|||||||
finished?: boolean;
|
finished?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EVENT_PRESET_DATA: Record<
|
|
||||||
string,
|
|
||||||
{ text: string; data: Record<string, unknown> }
|
|
||||||
> = {
|
|
||||||
'message.received': {
|
|
||||||
text: '',
|
|
||||||
data: {},
|
|
||||||
},
|
|
||||||
'group.member_joined': {
|
|
||||||
text: 'A new member joined the group.',
|
|
||||||
data: {
|
|
||||||
group_id: 'debug-group',
|
|
||||||
member_id: 'debug-user',
|
|
||||||
member_name: 'Debug User',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'group.member_left': {
|
|
||||||
text: 'A member left the group.',
|
|
||||||
data: {
|
|
||||||
group_id: 'debug-group',
|
|
||||||
member_id: 'debug-user',
|
|
||||||
member_name: 'Debug User',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'friend.request_received': {
|
|
||||||
text: 'A user sent a friend request.',
|
|
||||||
data: {
|
|
||||||
requester_id: 'debug-user',
|
|
||||||
requester_name: 'Debug User',
|
|
||||||
request_id: 'debug-friend-request',
|
|
||||||
message: 'Hello',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'feedback.received': {
|
|
||||||
text: 'The user submitted feedback.',
|
|
||||||
data: {
|
|
||||||
rating: 5,
|
|
||||||
content: 'Debug feedback',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'friend.added': {
|
|
||||||
text: 'A friend was added.',
|
|
||||||
data: { user_id: 'debug-user', user_name: 'Debug User' },
|
|
||||||
},
|
|
||||||
'group.member_banned': {
|
|
||||||
text: 'A member was banned.',
|
|
||||||
data: {
|
|
||||||
group_id: 'debug-group',
|
|
||||||
member_id: 'debug-user',
|
|
||||||
member_name: 'Debug User',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'bot.invited_to_group': {
|
|
||||||
text: 'The bot was invited to a group.',
|
|
||||||
data: {
|
|
||||||
group_id: 'debug-group',
|
|
||||||
request_id: 'debug-group-request',
|
|
||||||
requester_id: 'debug-user',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'bot.muted': {
|
|
||||||
text: 'The bot was muted.',
|
|
||||||
data: { group_id: 'debug-group', duration: 60 },
|
|
||||||
},
|
|
||||||
'bot.unmuted': {
|
|
||||||
text: 'The bot was unmuted.',
|
|
||||||
data: { group_id: 'debug-group' },
|
|
||||||
},
|
|
||||||
'bot.removed_from_group': {
|
|
||||||
text: 'The bot was removed from the group.',
|
|
||||||
data: { group_id: 'debug-group' },
|
|
||||||
},
|
|
||||||
'message.edited': {
|
|
||||||
text: 'A message was edited.',
|
|
||||||
data: {
|
|
||||||
group_id: 'debug-group',
|
|
||||||
message_id: 'debug-message',
|
|
||||||
text: 'Edited message',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'message.deleted': {
|
|
||||||
text: 'A message was deleted.',
|
|
||||||
data: { group_id: 'debug-group', message_id: 'debug-message' },
|
|
||||||
},
|
|
||||||
'message.reaction': {
|
|
||||||
text: 'A reaction was added.',
|
|
||||||
data: {
|
|
||||||
group_id: 'debug-group',
|
|
||||||
message_id: 'debug-message',
|
|
||||||
reaction: '👍',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'platform.specific': {
|
|
||||||
text: 'A platform-specific event occurred.',
|
|
||||||
data: { event_name: 'debug-platform-event' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function createDebugSessionId(agentId: string) {
|
function createDebugSessionId(agentId: string) {
|
||||||
const nonce = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
|
const nonce = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
|
||||||
return `webui:${agentId}:${nonce}`;
|
return `webui:${agentId}:${nonce}`;
|
||||||
@@ -186,8 +102,22 @@ export default function AgentDebugPanel({
|
|||||||
);
|
);
|
||||||
const [preset, setPreset] = useState('message.received');
|
const [preset, setPreset] = useState('message.received');
|
||||||
const [customEventType, setCustomEventType] = useState('custom.event');
|
const [customEventType, setCustomEventType] = useState('custom.event');
|
||||||
const [inputText, setInputText] = useState('');
|
const newEventData = useCallback(
|
||||||
const [eventDataText, setEventDataText] = useState('{}');
|
(type: string) =>
|
||||||
|
JSON.stringify(
|
||||||
|
createDebugEventData(type, {
|
||||||
|
user: t('agents.debugData.sampleUser'),
|
||||||
|
message: t('agents.debugData.sampleMessage'),
|
||||||
|
feedback: t('agents.debugData.sampleFeedback'),
|
||||||
|
}),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
const [eventDataText, setEventDataText] = useState(() =>
|
||||||
|
newEventData('message.received'),
|
||||||
|
);
|
||||||
const [mockOptionsText, setMockOptionsText] = useState('{}');
|
const [mockOptionsText, setMockOptionsText] = useState('{}');
|
||||||
const [running, setRunning] = useState(false);
|
const [running, setRunning] = useState(false);
|
||||||
const [entries, setEntries] = useState<DebugEntry[]>([]);
|
const [entries, setEntries] = useState<DebugEntry[]>([]);
|
||||||
@@ -205,11 +135,7 @@ export default function AgentDebugPanel({
|
|||||||
}, [entries]);
|
}, [entries]);
|
||||||
|
|
||||||
const eventType = preset === 'custom' ? customEventType.trim() : preset;
|
const eventType = preset === 'custom' ? customEventType.trim() : preset;
|
||||||
const isMessageEvent = eventType.startsWith('message.');
|
const eventDataValid = parseDebugEventData(eventDataText) !== null;
|
||||||
const supportedLabel = useMemo(
|
|
||||||
() => supportedEventPatterns.join(', '),
|
|
||||||
[supportedEventPatterns],
|
|
||||||
);
|
|
||||||
const availableEvents = useMemo(() => {
|
const availableEvents = useMemo(() => {
|
||||||
const concretePatterns = supportedEventPatterns.filter(
|
const concretePatterns = supportedEventPatterns.filter(
|
||||||
(pattern) => pattern !== '*' && !pattern.endsWith('.*'),
|
(pattern) => pattern !== '*' && !pattern.endsWith('.*'),
|
||||||
@@ -230,6 +156,14 @@ export default function AgentDebugPanel({
|
|||||||
(pattern) => pattern === '*' || pattern.endsWith('.*'),
|
(pattern) => pattern === '*' || pattern.endsWith('.*'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const selectPreset = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
setPreset(value);
|
||||||
|
setEventDataText(newEventData(value));
|
||||||
|
},
|
||||||
|
[newEventData],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
availableEvents.includes(preset) ||
|
availableEvents.includes(preset) ||
|
||||||
@@ -238,24 +172,13 @@ export default function AgentDebugPanel({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
selectPreset(availableEvents[0] ?? 'custom');
|
selectPreset(availableEvents[0] ?? 'custom');
|
||||||
}, [availableEvents, preset, supportsCustomEvent]);
|
}, [availableEvents, preset, supportsCustomEvent, selectPreset]);
|
||||||
|
|
||||||
function selectPreset(value: string) {
|
|
||||||
setPreset(value);
|
|
||||||
const nextPreset = EVENT_PRESET_DATA[value] ?? { text: '', data: {} };
|
|
||||||
setInputText(nextPreset.text);
|
|
||||||
setEventDataText(JSON.stringify(nextPreset.data, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runDebugEvent() {
|
async function runDebugEvent() {
|
||||||
if (!eventType) {
|
if (!eventType) {
|
||||||
toast.error(t('agents.debugEventTypeRequired'));
|
toast.error(t('agents.debugEventTypeRequired'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isMessageEvent && !inputText.trim()) {
|
|
||||||
toast.error(t('agents.debugInputRequired'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
!supportedEventPatterns.some((pattern) =>
|
!supportedEventPatterns.some((pattern) =>
|
||||||
matchesEventPattern(pattern, eventType),
|
matchesEventPattern(pattern, eventType),
|
||||||
@@ -265,18 +188,22 @@ export default function AgentDebugPanel({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let eventData: Record<string, unknown>;
|
const eventData = parseDebugEventData(eventDataText);
|
||||||
let mockOptions: Record<string, unknown>;
|
if (!eventData) {
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(eventDataText || '{}');
|
|
||||||
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
|
|
||||||
throw new Error('payload must be an object');
|
|
||||||
}
|
|
||||||
eventData = parsed as Record<string, unknown>;
|
|
||||||
} catch {
|
|
||||||
toast.error(t('agents.debugInvalidPayload'));
|
toast.error(t('agents.debugInvalidPayload'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const invalidField = invalidDebugEventField(eventType, eventData);
|
||||||
|
if (invalidField) {
|
||||||
|
toast.error(
|
||||||
|
t('agents.debugData.invalidField', {
|
||||||
|
field: t(`agents.debugData.${invalidField.label}`),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const inputText = debugEventInputText(eventType, eventData);
|
||||||
|
let mockOptions: Record<string, unknown>;
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(mockOptionsText || '{}');
|
const parsed = JSON.parse(mockOptionsText || '{}');
|
||||||
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object')
|
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object')
|
||||||
@@ -367,8 +294,6 @@ export default function AgentDebugPanel({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
if (isMessageEvent)
|
|
||||||
setInputText((current) => (current === inputText ? '' : current));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (controller.signal.aborted) {
|
if (controller.signal.aborted) {
|
||||||
if (controller.signal.reason === 'user') {
|
if (controller.signal.reason === 'user') {
|
||||||
@@ -541,131 +466,115 @@ export default function AgentDebugPanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="shrink-0 space-y-3 border-t p-3">
|
<div className="flex max-h-[60%] min-h-0 shrink-0 flex-col border-t">
|
||||||
{supportedEventPatterns.length === 0 ? (
|
<div className="min-h-0 space-y-3 overflow-y-auto p-3">
|
||||||
<Alert className="bg-amber-500/5 text-amber-800 dark:text-amber-200">
|
{supportedEventPatterns.length === 0 ? (
|
||||||
<AlertTriangle className="size-4" />
|
<Alert className="bg-amber-500/5 text-amber-800 dark:text-amber-200">
|
||||||
<AlertTitle>{t('agents.debugNoEventsTitle')}</AlertTitle>
|
<AlertTriangle className="size-4" />
|
||||||
<AlertDescription>
|
<AlertTitle>{t('agents.debugNoEventsTitle')}</AlertTitle>
|
||||||
{t('agents.debugNoEventsDescription')}
|
<AlertDescription>
|
||||||
</AlertDescription>
|
{t('agents.debugNoEventsDescription')}
|
||||||
</Alert>
|
</AlertDescription>
|
||||||
) : (
|
</Alert>
|
||||||
<>
|
) : (
|
||||||
<div className="space-y-1.5">
|
<>
|
||||||
<Label>{t('agents.debugEventType')}</Label>
|
<div className="space-y-1.5">
|
||||||
<Select value={preset} onValueChange={selectPreset}>
|
<Label>{t('agents.debugEventType')}</Label>
|
||||||
<SelectTrigger
|
<Select value={preset} onValueChange={selectPreset}>
|
||||||
className="w-full"
|
<SelectTrigger
|
||||||
aria-label={t('agents.debugEventType')}
|
className="w-full"
|
||||||
>
|
aria-label={t('agents.debugEventType')}
|
||||||
<SelectValue />
|
>
|
||||||
</SelectTrigger>
|
<SelectValue />
|
||||||
<SelectContent className="w-[var(--radix-select-trigger-width)] max-w-[calc(100vw-2rem)]">
|
</SelectTrigger>
|
||||||
{eventGroups.map((group) => (
|
<SelectContent className="w-[var(--radix-select-trigger-width)] max-w-[calc(100vw-2rem)]">
|
||||||
<SelectGroup key={group.namespace}>
|
{eventGroups.map((group) => (
|
||||||
<SelectLabel>
|
<SelectGroup key={group.namespace}>
|
||||||
{eventGroupLabel(group.namespace, t)}
|
<SelectLabel>
|
||||||
</SelectLabel>
|
{eventGroupLabel(group.namespace, t)}
|
||||||
{group.patterns.map((event) => (
|
</SelectLabel>
|
||||||
|
{group.patterns.map((event) => (
|
||||||
|
<SelectItem
|
||||||
|
key={event}
|
||||||
|
value={event}
|
||||||
|
description={eventPatternDescription(event, t)}
|
||||||
|
className="py-2"
|
||||||
|
>
|
||||||
|
<EventSelectOptionContent
|
||||||
|
event={event}
|
||||||
|
label={eventPatternLabel(event, t)}
|
||||||
|
/>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
))}
|
||||||
|
{supportsCustomEvent && (
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>
|
||||||
|
{t('agents.debugCustomEvent')}
|
||||||
|
</SelectLabel>
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={event}
|
value="custom"
|
||||||
value={event}
|
description={t('bots.eventDescriptions.custom')}
|
||||||
description={eventPatternDescription(event, t)}
|
|
||||||
className="py-2"
|
className="py-2"
|
||||||
>
|
>
|
||||||
<EventSelectOptionContent
|
<EventSelectOptionContent
|
||||||
event={event}
|
event="custom.event"
|
||||||
label={eventPatternLabel(event, t)}
|
label={t('agents.debugCustomEvent')}
|
||||||
/>
|
/>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
</SelectGroup>
|
||||||
</SelectGroup>
|
)}
|
||||||
))}
|
</SelectContent>
|
||||||
{supportsCustomEvent && (
|
</Select>
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>{t('agents.debugCustomEvent')}</SelectLabel>
|
|
||||||
<SelectItem
|
|
||||||
value="custom"
|
|
||||||
description={t('bots.eventDescriptions.custom')}
|
|
||||||
className="py-2"
|
|
||||||
>
|
|
||||||
<EventSelectOptionContent
|
|
||||||
event="custom.event"
|
|
||||||
label={t('agents.debugCustomEvent')}
|
|
||||||
/>
|
|
||||||
</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{preset === 'custom' && (
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label htmlFor="agent-debug-custom-event">
|
|
||||||
{t('agents.debugCustomEventType')}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="agent-debug-custom-event"
|
|
||||||
value={customEventType}
|
|
||||||
onChange={(event) => setCustomEventType(event.target.value)}
|
|
||||||
placeholder="custom.event"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
{preset === 'custom' && (
|
||||||
<Label htmlFor="agent-debug-input">
|
<div className="space-y-1.5">
|
||||||
{isMessageEvent
|
<Label htmlFor="agent-debug-custom-event">
|
||||||
? t('agents.debugMessageInput')
|
{t('agents.debugCustomEventType')}
|
||||||
: t('agents.debugEventSummary')}
|
</Label>
|
||||||
</Label>
|
<Input
|
||||||
<Textarea
|
id="agent-debug-custom-event"
|
||||||
id="agent-debug-input"
|
value={customEventType}
|
||||||
value={inputText}
|
onChange={(event) => setCustomEventType(event.target.value)}
|
||||||
onChange={(event) => setInputText(event.target.value)}
|
placeholder="custom.event"
|
||||||
className="min-h-20 resize-y"
|
/>
|
||||||
placeholder={t('agents.debugInputPlaceholder')}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AgentEventDataEditor
|
||||||
|
key={eventType}
|
||||||
|
eventType={eventType}
|
||||||
|
custom={preset === 'custom'}
|
||||||
|
value={eventDataText}
|
||||||
|
onChange={setEventDataText}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<details className="rounded-md border bg-muted/20 px-3 py-2">
|
<details className="text-muted-foreground">
|
||||||
<summary className="cursor-pointer text-xs font-medium">
|
<summary className="cursor-pointer text-xs font-medium">
|
||||||
{t('agents.debugEventPayload')}
|
{t('agents.debugMockOptions')}
|
||||||
</summary>
|
</summary>
|
||||||
<div className="mt-2 space-y-2">
|
<p className="my-2 text-xs text-muted-foreground">
|
||||||
<p className="text-xs text-muted-foreground">
|
{t('agents.debugMockOptionsHelp')}
|
||||||
{t('agents.debugSupportedEvents')}: {supportedLabel}
|
|
||||||
</p>
|
</p>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="agent-debug-payload"
|
aria-label={t('agents.debugMockOptions')}
|
||||||
value={eventDataText}
|
value={mockOptionsText}
|
||||||
onChange={(event) => setEventDataText(event.target.value)}
|
onChange={(event) => setMockOptionsText(event.target.value)}
|
||||||
className="min-h-28 resize-y font-mono text-xs"
|
className="min-h-24 font-mono text-xs"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</details>
|
||||||
</details>
|
</>
|
||||||
|
)}
|
||||||
<details className="rounded-md border bg-muted/20 px-3 py-2">
|
</div>
|
||||||
<summary className="cursor-pointer text-xs font-medium">
|
{supportedEventPatterns.length > 0 && (
|
||||||
{t('agents.debugMockOptions')}
|
<div className="shrink-0 px-3 pb-3">
|
||||||
</summary>
|
|
||||||
<p className="my-2 text-xs text-muted-foreground">
|
|
||||||
{t('agents.debugMockOptionsHelp')}
|
|
||||||
</p>
|
|
||||||
<Textarea
|
|
||||||
aria-label={t('agents.debugMockOptions')}
|
|
||||||
value={mockOptionsText}
|
|
||||||
onChange={(event) => setMockOptionsText(event.target.value)}
|
|
||||||
className="min-h-24 font-mono text-xs"
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
</details>
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
disabled={!running && !eventDataValid}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
running ? requestRef.current?.abort('user') : runDebugEvent()
|
running ? requestRef.current?.abort('user') : runDebugEvent()
|
||||||
}
|
}
|
||||||
@@ -681,7 +590,7 @@ export default function AgentDebugPanel({
|
|||||||
? t('agents.debugSaveAndRun')
|
? t('agents.debugSaveAndRun')
|
||||||
: t('agents.debugRun')}
|
: t('agents.debugRun')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ArrowLeftRight } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { debugEventDefinition, parseDebugEventData } from './debug-event-data';
|
||||||
|
|
||||||
|
export default function AgentEventDataEditor({
|
||||||
|
eventType,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
custom = false,
|
||||||
|
}: {
|
||||||
|
eventType: string;
|
||||||
|
custom?: boolean;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [showJson, setShowJson] = useState(false);
|
||||||
|
const fields = custom ? [] : (debugEventDefinition(eventType)?.fields ?? []);
|
||||||
|
const data = parseDebugEventData(value);
|
||||||
|
const jsonMode = showJson || !fields.length;
|
||||||
|
|
||||||
|
function updateField(key: string, next: string | number | undefined) {
|
||||||
|
if (data) onChange(JSON.stringify({ ...data, [key]: next }, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-xs font-medium">
|
||||||
|
{t('agents.debugData.title')}
|
||||||
|
</span>
|
||||||
|
{fields.length > 0 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 gap-1.5 px-1.5 text-xs text-muted-foreground"
|
||||||
|
disabled={jsonMode && !data}
|
||||||
|
onClick={() => setShowJson(!showJson)}
|
||||||
|
>
|
||||||
|
<ArrowLeftRight className="size-3.5" aria-hidden="true" />
|
||||||
|
{t(jsonMode ? 'agents.debugData.form' : 'agents.debugData.json')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{jsonMode ? (
|
||||||
|
<>
|
||||||
|
<Textarea
|
||||||
|
aria-label={t('agents.debugData.json')}
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
rows={6}
|
||||||
|
className="min-h-28 font-mono text-xs"
|
||||||
|
spellCheck={false}
|
||||||
|
aria-invalid={!data}
|
||||||
|
/>
|
||||||
|
{!data && (
|
||||||
|
<p role="alert" className="text-xs text-destructive">
|
||||||
|
{t('agents.debugInvalidPayload')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-2 gap-x-3 gap-y-2">
|
||||||
|
{fields.map((field) => {
|
||||||
|
const id = `agent-debug-data-${field.key}`;
|
||||||
|
const current = data?.[field.key];
|
||||||
|
const text =
|
||||||
|
typeof current === 'string' || typeof current === 'number'
|
||||||
|
? current
|
||||||
|
: '';
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={field.key}
|
||||||
|
className={`min-w-0 space-y-1 ${field.multiline ? 'col-span-2' : ''}`}
|
||||||
|
>
|
||||||
|
<Label htmlFor={id} className="text-xs text-muted-foreground">
|
||||||
|
{t(`agents.debugData.${field.label}`)}
|
||||||
|
</Label>
|
||||||
|
{field.multiline ? (
|
||||||
|
<Textarea
|
||||||
|
id={id}
|
||||||
|
value={text}
|
||||||
|
rows={2}
|
||||||
|
required={field.required}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateField(field.key, event.target.value)
|
||||||
|
}
|
||||||
|
className="min-h-16 resize-y text-sm"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
id={id}
|
||||||
|
value={text}
|
||||||
|
type={typeof field.value === 'number' ? 'number' : 'text'}
|
||||||
|
min={field.min}
|
||||||
|
max={field.max}
|
||||||
|
step={1}
|
||||||
|
className="h-8 text-sm"
|
||||||
|
placeholder={
|
||||||
|
field.placeholder
|
||||||
|
? t(`agents.debugData.${field.placeholder}`)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateField(
|
||||||
|
field.key,
|
||||||
|
typeof field.value === 'number'
|
||||||
|
? event.target.value === ''
|
||||||
|
? undefined
|
||||||
|
: Number(event.target.value)
|
||||||
|
: event.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
export interface DebugEventField {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
sample?: 'user' | 'message' | 'feedback';
|
||||||
|
multiline?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DebugEventDefinition {
|
||||||
|
fields: DebugEventField[];
|
||||||
|
defaults?: Record<string, unknown>;
|
||||||
|
messageField?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const group: DebugEventField = {
|
||||||
|
key: 'group_id',
|
||||||
|
label: 'groupId',
|
||||||
|
value: 'debug-group',
|
||||||
|
};
|
||||||
|
const member: DebugEventField = {
|
||||||
|
key: 'member_id',
|
||||||
|
label: 'memberId',
|
||||||
|
value: 'debug-user',
|
||||||
|
};
|
||||||
|
const memberName: DebugEventField = {
|
||||||
|
key: 'member_name',
|
||||||
|
label: 'memberName',
|
||||||
|
value: '',
|
||||||
|
sample: 'user',
|
||||||
|
};
|
||||||
|
const user: DebugEventField = {
|
||||||
|
key: 'user_id',
|
||||||
|
label: 'userId',
|
||||||
|
value: 'debug-user',
|
||||||
|
};
|
||||||
|
const userName: DebugEventField = {
|
||||||
|
key: 'user_name',
|
||||||
|
label: 'userName',
|
||||||
|
value: '',
|
||||||
|
sample: 'user',
|
||||||
|
};
|
||||||
|
const requester: DebugEventField = {
|
||||||
|
key: 'requester_id',
|
||||||
|
label: 'requesterId',
|
||||||
|
value: 'debug-user',
|
||||||
|
};
|
||||||
|
const messageId: DebugEventField = {
|
||||||
|
key: 'message_id',
|
||||||
|
label: 'messageId',
|
||||||
|
value: 'debug-message',
|
||||||
|
};
|
||||||
|
const duration: DebugEventField = {
|
||||||
|
key: 'duration',
|
||||||
|
label: 'duration',
|
||||||
|
value: 60,
|
||||||
|
min: 0,
|
||||||
|
};
|
||||||
|
const message: DebugEventField = {
|
||||||
|
key: 'text',
|
||||||
|
label: 'message',
|
||||||
|
value: '',
|
||||||
|
sample: 'message',
|
||||||
|
multiline: true,
|
||||||
|
required: true,
|
||||||
|
};
|
||||||
|
const membership: DebugEventDefinition = {
|
||||||
|
fields: [memberName, member, group],
|
||||||
|
};
|
||||||
|
const friendship: DebugEventDefinition = { fields: [userName, user] };
|
||||||
|
const deletion: DebugEventDefinition = { fields: [messageId, group] };
|
||||||
|
|
||||||
|
// The compact form projects the existing debug API data fields. Fields omitted
|
||||||
|
// from the form, including custom fields, remain editable in the same JSON data.
|
||||||
|
const DEBUG_EVENT_DEFINITIONS: Record<string, DebugEventDefinition> = {
|
||||||
|
'message.received': {
|
||||||
|
fields: [
|
||||||
|
message,
|
||||||
|
userName,
|
||||||
|
{ ...group, value: '', placeholder: 'privateChat' },
|
||||||
|
],
|
||||||
|
defaults: { user_id: 'debug-user' },
|
||||||
|
messageField: 'text',
|
||||||
|
},
|
||||||
|
'message.edited': {
|
||||||
|
fields: [{ ...message, label: 'newMessage' }, messageId, group],
|
||||||
|
messageField: 'text',
|
||||||
|
},
|
||||||
|
'message.deleted': deletion,
|
||||||
|
'message.recalled': deletion,
|
||||||
|
'message.reaction': {
|
||||||
|
fields: [
|
||||||
|
{ key: 'reaction', label: 'reaction', value: '👍' },
|
||||||
|
messageId,
|
||||||
|
group,
|
||||||
|
],
|
||||||
|
defaults: { is_add: true },
|
||||||
|
},
|
||||||
|
'group.member_joined': membership,
|
||||||
|
'group.member_left': membership,
|
||||||
|
'group.member_banned': { fields: [member, group, duration] },
|
||||||
|
'group.info_updated': {
|
||||||
|
fields: [
|
||||||
|
{ key: 'group_name', label: 'groupName', value: 'debug-group' },
|
||||||
|
group,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'friend.request_received': {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'message',
|
||||||
|
label: 'verificationMessage',
|
||||||
|
value: '',
|
||||||
|
sample: 'message',
|
||||||
|
multiline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'requester_name',
|
||||||
|
label: 'requesterName',
|
||||||
|
value: '',
|
||||||
|
sample: 'user',
|
||||||
|
},
|
||||||
|
requester,
|
||||||
|
],
|
||||||
|
defaults: { request_id: 'debug-friend-request' },
|
||||||
|
},
|
||||||
|
'friend.added': friendship,
|
||||||
|
'friend.removed': friendship,
|
||||||
|
'bot.invited_to_group': {
|
||||||
|
fields: [group, requester],
|
||||||
|
defaults: { request_id: 'debug-group-request' },
|
||||||
|
},
|
||||||
|
'bot.muted': { fields: [group, duration] },
|
||||||
|
'bot.unmuted': { fields: [group] },
|
||||||
|
'bot.removed_from_group': { fields: [group] },
|
||||||
|
'feedback.received': {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'content',
|
||||||
|
label: 'feedback',
|
||||||
|
value: '',
|
||||||
|
sample: 'feedback',
|
||||||
|
multiline: true,
|
||||||
|
},
|
||||||
|
{ key: 'rating', label: 'rating', value: 5, min: 1, max: 5 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'platform.specific': {
|
||||||
|
fields: [
|
||||||
|
{ key: 'event_name', label: 'eventName', value: 'debug-platform-event' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function debugEventDefinition(
|
||||||
|
eventType: string,
|
||||||
|
): DebugEventDefinition | undefined {
|
||||||
|
return Object.hasOwn(DEBUG_EVENT_DEFINITIONS, eventType)
|
||||||
|
? DEBUG_EVENT_DEFINITIONS[eventType]
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDebugEventData(
|
||||||
|
eventType: string,
|
||||||
|
samples: Record<'user' | 'message' | 'feedback', string>,
|
||||||
|
) {
|
||||||
|
const definition = debugEventDefinition(eventType);
|
||||||
|
return {
|
||||||
|
...definition?.defaults,
|
||||||
|
...Object.fromEntries(
|
||||||
|
(definition?.fields ?? []).map((field) => [
|
||||||
|
field.key,
|
||||||
|
field.sample ? samples[field.sample] : field.value,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseDebugEventData(
|
||||||
|
text: string,
|
||||||
|
): Record<string, unknown> | null {
|
||||||
|
try {
|
||||||
|
const value = JSON.parse(text);
|
||||||
|
return value && !Array.isArray(value) && typeof value === 'object'
|
||||||
|
? value
|
||||||
|
: null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invalidDebugEventField(
|
||||||
|
eventType: string,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
return debugEventDefinition(eventType)?.fields.find((field) => {
|
||||||
|
const value = data[field.key];
|
||||||
|
if (
|
||||||
|
value === undefined ||
|
||||||
|
value === null ||
|
||||||
|
(typeof value === 'string' && !value.trim())
|
||||||
|
)
|
||||||
|
return field.required;
|
||||||
|
if (typeof field.value === 'number') {
|
||||||
|
return (
|
||||||
|
typeof value !== 'number' ||
|
||||||
|
!Number.isFinite(value) ||
|
||||||
|
!Number.isInteger(value) ||
|
||||||
|
(field.min !== undefined && value < field.min) ||
|
||||||
|
(field.max !== undefined && value > field.max)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
typeof value !== 'string' &&
|
||||||
|
!(field.key.endsWith('_id') && typeof value === 'number')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debugEventInputText(
|
||||||
|
eventType: string,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
const key = debugEventDefinition(eventType)?.messageField;
|
||||||
|
return key && typeof data[key] === 'string' ? data[key].trim() : '';
|
||||||
|
}
|
||||||
@@ -704,6 +704,33 @@ const enUS = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
|
debugData: {
|
||||||
|
title: 'Event data',
|
||||||
|
form: 'Common fields',
|
||||||
|
json: 'Full JSON',
|
||||||
|
groupId: 'Group ID',
|
||||||
|
memberId: 'Member ID',
|
||||||
|
memberName: 'Member name',
|
||||||
|
userId: 'User ID',
|
||||||
|
userName: 'User name',
|
||||||
|
requesterId: 'Requester ID',
|
||||||
|
requesterName: 'Requester name',
|
||||||
|
messageId: 'Message ID',
|
||||||
|
duration: 'Duration (seconds)',
|
||||||
|
message: 'Message content',
|
||||||
|
newMessage: 'New message content',
|
||||||
|
verificationMessage: 'Verification message',
|
||||||
|
reaction: 'Reaction',
|
||||||
|
groupName: 'Group name',
|
||||||
|
feedback: 'Feedback',
|
||||||
|
rating: 'Rating',
|
||||||
|
eventName: 'Event name',
|
||||||
|
privateChat: 'Leave empty for private chat',
|
||||||
|
sampleUser: 'Test User',
|
||||||
|
sampleMessage: 'Hello',
|
||||||
|
sampleFeedback: 'Very helpful',
|
||||||
|
invalidField: 'Check {{field}}',
|
||||||
|
},
|
||||||
title: 'Processors',
|
title: 'Processors',
|
||||||
description: 'Create reusable processors and use them in bot event routing',
|
description: 'Create reusable processors and use them in bot event routing',
|
||||||
create: 'Create Processor',
|
create: 'Create Processor',
|
||||||
|
|||||||
@@ -506,6 +506,33 @@ const esES = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
|
debugData: {
|
||||||
|
title: 'Datos del evento',
|
||||||
|
form: 'Campos comunes',
|
||||||
|
json: 'JSON completo',
|
||||||
|
groupId: 'ID del grupo',
|
||||||
|
memberId: 'ID del miembro',
|
||||||
|
memberName: 'Nombre del miembro',
|
||||||
|
userId: 'ID del usuario',
|
||||||
|
userName: 'Nombre del usuario',
|
||||||
|
requesterId: 'ID del solicitante',
|
||||||
|
requesterName: 'Nombre del solicitante',
|
||||||
|
messageId: 'ID del mensaje',
|
||||||
|
duration: 'Duración (segundos)',
|
||||||
|
message: 'Contenido del mensaje',
|
||||||
|
newMessage: 'Contenido editado',
|
||||||
|
verificationMessage: 'Mensaje de verificación',
|
||||||
|
reaction: 'Reacción',
|
||||||
|
groupName: 'Nombre del grupo',
|
||||||
|
feedback: 'Comentarios',
|
||||||
|
rating: 'Puntuación',
|
||||||
|
eventName: 'Nombre del evento',
|
||||||
|
privateChat: 'Vacío para chat privado',
|
||||||
|
sampleUser: 'Usuario de prueba',
|
||||||
|
sampleMessage: 'Hola',
|
||||||
|
sampleFeedback: 'Muy útil',
|
||||||
|
invalidField: 'Revisa {{field}}',
|
||||||
|
},
|
||||||
title: 'Procesadores',
|
title: 'Procesadores',
|
||||||
description:
|
description:
|
||||||
'Crea procesadores reutilizables y úsalos en el enrutamiento de eventos del bot',
|
'Crea procesadores reutilizables y úsalos en el enrutamiento de eventos del bot',
|
||||||
|
|||||||
@@ -716,6 +716,33 @@ const jaJP = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
|
debugData: {
|
||||||
|
title: 'イベントデータ',
|
||||||
|
form: '基本項目',
|
||||||
|
json: '完全な JSON',
|
||||||
|
groupId: 'グループ ID',
|
||||||
|
memberId: 'メンバー ID',
|
||||||
|
memberName: 'メンバー名',
|
||||||
|
userId: 'ユーザー ID',
|
||||||
|
userName: 'ユーザー名',
|
||||||
|
requesterId: '申請者 ID',
|
||||||
|
requesterName: '申請者名',
|
||||||
|
messageId: 'メッセージ ID',
|
||||||
|
duration: '時間(秒)',
|
||||||
|
message: 'メッセージ内容',
|
||||||
|
newMessage: '編集後の内容',
|
||||||
|
verificationMessage: '申請メッセージ',
|
||||||
|
reaction: 'リアクション',
|
||||||
|
groupName: 'グループ名',
|
||||||
|
feedback: 'フィードバック',
|
||||||
|
rating: '評価',
|
||||||
|
eventName: 'イベント名',
|
||||||
|
privateChat: '空欄で個別チャット',
|
||||||
|
sampleUser: 'テストユーザー',
|
||||||
|
sampleMessage: 'こんにちは',
|
||||||
|
sampleFeedback: 'とても役立ちました',
|
||||||
|
invalidField: '{{field}}を確認してください',
|
||||||
|
},
|
||||||
debugReasoning: '思考内容',
|
debugReasoning: '思考内容',
|
||||||
debugTextOutput: 'テキスト出力',
|
debugTextOutput: 'テキスト出力',
|
||||||
debugPlatformNotice:
|
debugPlatformNotice:
|
||||||
|
|||||||
@@ -503,6 +503,33 @@ const ruRU = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
|
debugData: {
|
||||||
|
title: 'Данные события',
|
||||||
|
form: 'Основные поля',
|
||||||
|
json: 'Полный JSON',
|
||||||
|
groupId: 'ID группы',
|
||||||
|
memberId: 'ID участника',
|
||||||
|
memberName: 'Имя участника',
|
||||||
|
userId: 'ID пользователя',
|
||||||
|
userName: 'Имя пользователя',
|
||||||
|
requesterId: 'ID заявителя',
|
||||||
|
requesterName: 'Имя заявителя',
|
||||||
|
messageId: 'ID сообщения',
|
||||||
|
duration: 'Длительность (секунды)',
|
||||||
|
message: 'Текст сообщения',
|
||||||
|
newMessage: 'Новый текст',
|
||||||
|
verificationMessage: 'Текст заявки',
|
||||||
|
reaction: 'Реакция',
|
||||||
|
groupName: 'Название группы',
|
||||||
|
feedback: 'Отзыв',
|
||||||
|
rating: 'Оценка',
|
||||||
|
eventName: 'Название события',
|
||||||
|
privateChat: 'Пусто для личного чата',
|
||||||
|
sampleUser: 'Тестовый пользователь',
|
||||||
|
sampleMessage: 'Привет',
|
||||||
|
sampleFeedback: 'Очень полезно',
|
||||||
|
invalidField: 'Проверьте {{field}}',
|
||||||
|
},
|
||||||
title: 'Обработчики',
|
title: 'Обработчики',
|
||||||
description:
|
description:
|
||||||
'Создавайте переиспользуемые обработчики и используйте их в маршрутизации событий бота',
|
'Создавайте переиспользуемые обработчики и используйте их в маршрутизации событий бота',
|
||||||
|
|||||||
@@ -490,6 +490,33 @@ const thTH = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
|
debugData: {
|
||||||
|
title: 'ข้อมูลเหตุการณ์',
|
||||||
|
form: 'ฟิลด์ทั่วไป',
|
||||||
|
json: 'JSON ทั้งหมด',
|
||||||
|
groupId: 'ID กลุ่ม',
|
||||||
|
memberId: 'ID สมาชิก',
|
||||||
|
memberName: 'ชื่อสมาชิก',
|
||||||
|
userId: 'ID ผู้ใช้',
|
||||||
|
userName: 'ชื่อผู้ใช้',
|
||||||
|
requesterId: 'ID ผู้ส่งคำขอ',
|
||||||
|
requesterName: 'ชื่อผู้ส่งคำขอ',
|
||||||
|
messageId: 'ID ข้อความ',
|
||||||
|
duration: 'ระยะเวลา (วินาที)',
|
||||||
|
message: 'เนื้อหาข้อความ',
|
||||||
|
newMessage: 'เนื้อหาที่แก้ไข',
|
||||||
|
verificationMessage: 'ข้อความยืนยัน',
|
||||||
|
reaction: 'อีโมจิ',
|
||||||
|
groupName: 'ชื่อกลุ่ม',
|
||||||
|
feedback: 'ความคิดเห็น',
|
||||||
|
rating: 'คะแนน',
|
||||||
|
eventName: 'ชื่อเหตุการณ์',
|
||||||
|
privateChat: 'เว้นว่างสำหรับแชทส่วนตัว',
|
||||||
|
sampleUser: 'ผู้ใช้ทดสอบ',
|
||||||
|
sampleMessage: 'สวัสดี',
|
||||||
|
sampleFeedback: 'มีประโยชน์มาก',
|
||||||
|
invalidField: 'โปรดตรวจสอบ {{field}}',
|
||||||
|
},
|
||||||
title: 'ตัวประมวลผล',
|
title: 'ตัวประมวลผล',
|
||||||
description: 'สร้างตัวประมวลผลที่ใช้ซ้ำได้และใช้ในเส้นทางเหตุการณ์ของบอท',
|
description: 'สร้างตัวประมวลผลที่ใช้ซ้ำได้และใช้ในเส้นทางเหตุการณ์ของบอท',
|
||||||
create: 'สร้างตัวประมวลผล',
|
create: 'สร้างตัวประมวลผล',
|
||||||
|
|||||||
@@ -499,6 +499,33 @@ const viVN = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
|
debugData: {
|
||||||
|
title: 'Dữ liệu sự kiện',
|
||||||
|
form: 'Trường thường dùng',
|
||||||
|
json: 'JSON đầy đủ',
|
||||||
|
groupId: 'ID nhóm',
|
||||||
|
memberId: 'ID thành viên',
|
||||||
|
memberName: 'Tên thành viên',
|
||||||
|
userId: 'ID người dùng',
|
||||||
|
userName: 'Tên người dùng',
|
||||||
|
requesterId: 'ID người yêu cầu',
|
||||||
|
requesterName: 'Tên người yêu cầu',
|
||||||
|
messageId: 'ID tin nhắn',
|
||||||
|
duration: 'Thời lượng (giây)',
|
||||||
|
message: 'Nội dung tin nhắn',
|
||||||
|
newMessage: 'Nội dung đã sửa',
|
||||||
|
verificationMessage: 'Tin nhắn xác minh',
|
||||||
|
reaction: 'Biểu cảm',
|
||||||
|
groupName: 'Tên nhóm',
|
||||||
|
feedback: 'Phản hồi',
|
||||||
|
rating: 'Đánh giá',
|
||||||
|
eventName: 'Tên sự kiện',
|
||||||
|
privateChat: 'Để trống cho trò chuyện riêng',
|
||||||
|
sampleUser: 'Người dùng thử',
|
||||||
|
sampleMessage: 'Xin chào',
|
||||||
|
sampleFeedback: 'Rất hữu ích',
|
||||||
|
invalidField: 'Kiểm tra {{field}}',
|
||||||
|
},
|
||||||
title: 'Bộ xử lý',
|
title: 'Bộ xử lý',
|
||||||
description:
|
description:
|
||||||
'Tạo bộ xử lý có thể tái sử dụng và dùng chúng trong định tuyến sự kiện của bot',
|
'Tạo bộ xử lý có thể tái sử dụng và dùng chúng trong định tuyến sự kiện của bot',
|
||||||
|
|||||||
@@ -670,6 +670,33 @@ const zhHans = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
|
debugData: {
|
||||||
|
title: '事件数据',
|
||||||
|
form: '常用字段',
|
||||||
|
json: '完整 JSON',
|
||||||
|
groupId: '群组 ID',
|
||||||
|
memberId: '成员 ID',
|
||||||
|
memberName: '成员昵称',
|
||||||
|
userId: '用户 ID',
|
||||||
|
userName: '用户昵称',
|
||||||
|
requesterId: '申请者 ID',
|
||||||
|
requesterName: '申请者昵称',
|
||||||
|
messageId: '消息 ID',
|
||||||
|
duration: '时长(秒)',
|
||||||
|
message: '消息内容',
|
||||||
|
newMessage: '修改后的内容',
|
||||||
|
verificationMessage: '验证消息',
|
||||||
|
reaction: '表情',
|
||||||
|
groupName: '群组名称',
|
||||||
|
feedback: '反馈内容',
|
||||||
|
rating: '评分',
|
||||||
|
eventName: '事件名称',
|
||||||
|
privateChat: '留空为私聊',
|
||||||
|
sampleUser: '测试用户',
|
||||||
|
sampleMessage: '你好',
|
||||||
|
sampleFeedback: '很有帮助',
|
||||||
|
invalidField: '请检查{{field}}',
|
||||||
|
},
|
||||||
title: '处理器',
|
title: '处理器',
|
||||||
description: '创建可复用的处理器,并在机器人事件路由中使用',
|
description: '创建可复用的处理器,并在机器人事件路由中使用',
|
||||||
create: '创建处理器',
|
create: '创建处理器',
|
||||||
|
|||||||
@@ -474,6 +474,33 @@ const zhHant = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
|
debugData: {
|
||||||
|
title: '事件資料',
|
||||||
|
form: '常用欄位',
|
||||||
|
json: '完整 JSON',
|
||||||
|
groupId: '群組 ID',
|
||||||
|
memberId: '成員 ID',
|
||||||
|
memberName: '成員暱稱',
|
||||||
|
userId: '使用者 ID',
|
||||||
|
userName: '使用者暱稱',
|
||||||
|
requesterId: '申請者 ID',
|
||||||
|
requesterName: '申請者暱稱',
|
||||||
|
messageId: '訊息 ID',
|
||||||
|
duration: '時長(秒)',
|
||||||
|
message: '訊息內容',
|
||||||
|
newMessage: '修改後的內容',
|
||||||
|
verificationMessage: '驗證訊息',
|
||||||
|
reaction: '表情',
|
||||||
|
groupName: '群組名稱',
|
||||||
|
feedback: '回饋內容',
|
||||||
|
rating: '評分',
|
||||||
|
eventName: '事件名稱',
|
||||||
|
privateChat: '留空為私聊',
|
||||||
|
sampleUser: '測試使用者',
|
||||||
|
sampleMessage: '你好',
|
||||||
|
sampleFeedback: '很有幫助',
|
||||||
|
invalidField: '請檢查{{field}}',
|
||||||
|
},
|
||||||
title: '處理器',
|
title: '處理器',
|
||||||
description: '建立可重用的處理器,並在機器人事件路由中使用',
|
description: '建立可重用的處理器,並在機器人事件路由中使用',
|
||||||
create: '建立處理器',
|
create: '建立處理器',
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
import { installLangBotApiMocks } from './fixtures/langbot-api';
|
||||||
|
|
||||||
|
test('event data stays in sync across the compact form, JSON and the request', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await installLangBotApiMocks(page, {
|
||||||
|
authenticated: true,
|
||||||
|
withAdapterEvents: true,
|
||||||
|
});
|
||||||
|
await page.route(
|
||||||
|
'**/api/v1/agents/agent-workbench/debug/stream',
|
||||||
|
async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
contentType: 'application/x-ndjson',
|
||||||
|
body:
|
||||||
|
JSON.stringify({ kind: 'completed', data: { final_text: 'Done' } }) +
|
||||||
|
'\n',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await page.goto('/home/agents?id=agent-workbench');
|
||||||
|
const panel = page.getByRole('region', { name: 'Event Debug' });
|
||||||
|
await panel
|
||||||
|
.getByRole('textbox', { name: 'Message content', exact: true })
|
||||||
|
.fill('Hello Alice');
|
||||||
|
await panel.getByRole('button', { name: 'Full JSON' }).click();
|
||||||
|
const json = panel.getByRole('textbox', { name: 'Full JSON' });
|
||||||
|
expect(JSON.parse(await json.inputValue()).text).toBe('Hello Alice');
|
||||||
|
await json.fill('{');
|
||||||
|
await expect(panel.getByRole('button', { name: 'Run test' })).toBeDisabled();
|
||||||
|
await expect(panel.getByRole('alert').last()).toContainText('valid JSON');
|
||||||
|
await json.fill(
|
||||||
|
JSON.stringify({
|
||||||
|
text: 'Edited in JSON',
|
||||||
|
user_name: 'Alice',
|
||||||
|
extra: { keep: true },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await panel.getByRole('button', { name: 'Common fields' }).click();
|
||||||
|
await expect(
|
||||||
|
panel.getByRole('textbox', { name: 'Message content', exact: true }),
|
||||||
|
).toHaveValue('Edited in JSON');
|
||||||
|
await panel.getByRole('textbox', { name: 'User name' }).fill('Bob');
|
||||||
|
await panel.getByRole('button', { name: 'Full JSON' }).click();
|
||||||
|
expect(JSON.parse(await json.inputValue())).toMatchObject({
|
||||||
|
user_name: 'Bob',
|
||||||
|
extra: { keep: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
await panel.getByRole('combobox', { name: 'Event type' }).click();
|
||||||
|
await page
|
||||||
|
.getByRole('option')
|
||||||
|
.filter({ hasText: 'group.member_joined' })
|
||||||
|
.click();
|
||||||
|
await expect(
|
||||||
|
panel.getByRole('textbox', { name: 'Message content', exact: true }),
|
||||||
|
).toHaveCount(0);
|
||||||
|
await expect(
|
||||||
|
panel.getByRole('textbox', { name: 'Event summary' }),
|
||||||
|
).toHaveCount(0);
|
||||||
|
await panel.getByRole('textbox', { name: 'Member name' }).fill('Carol');
|
||||||
|
await panel.getByRole('textbox', { name: 'Member ID' }).fill('member-42');
|
||||||
|
await panel.getByRole('textbox', { name: 'Group ID' }).fill('group-42');
|
||||||
|
const request = page.waitForRequest(
|
||||||
|
(req) => req.method() === 'POST' && req.url().endsWith('/debug/stream'),
|
||||||
|
);
|
||||||
|
await panel.getByRole('button', { name: 'Run test' }).click();
|
||||||
|
expect((await request).postDataJSON()).toMatchObject({
|
||||||
|
event_type: 'group.member_joined',
|
||||||
|
text: '',
|
||||||
|
data: {
|
||||||
|
member_name: 'Carol',
|
||||||
|
member_id: 'member-42',
|
||||||
|
group_id: 'group-42',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await expect(panel.getByText('Done', { exact: true })).toBeVisible();
|
||||||
|
await page.setViewportSize({ width: 1280, height: 650 });
|
||||||
|
await panel.getByRole('button', { name: 'Full JSON' }).click();
|
||||||
|
await panel.locator('summary').filter({ hasText: 'Mock scenario' }).click();
|
||||||
|
await expect(
|
||||||
|
panel.getByRole('button', { name: 'Run test' }),
|
||||||
|
).toBeInViewport();
|
||||||
|
|
||||||
|
await panel.getByRole('combobox', { name: 'Event type' }).click();
|
||||||
|
await page.getByRole('option').filter({ hasText: 'Custom event' }).click();
|
||||||
|
await expect(panel.getByRole('textbox', { name: 'Full JSON' })).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
panel.getByRole('button', { name: 'Common fields' }),
|
||||||
|
).toHaveCount(0);
|
||||||
|
});
|
||||||
@@ -36,7 +36,7 @@ test.describe('processor detail workbench', () => {
|
|||||||
.boundingBox();
|
.boundingBox();
|
||||||
const eventPickerBox = await debugEventPicker.boundingBox();
|
const eventPickerBox = await debugEventPicker.boundingBox();
|
||||||
const conversationInputBox = await debugPanel
|
const conversationInputBox = await debugPanel
|
||||||
.getByRole('textbox', { name: 'Conversation input' })
|
.getByRole('textbox', { name: 'Message content' })
|
||||||
.boundingBox();
|
.boundingBox();
|
||||||
expect(transcriptBox).not.toBeNull();
|
expect(transcriptBox).not.toBeNull();
|
||||||
expect(eventPickerBox).not.toBeNull();
|
expect(eventPickerBox).not.toBeNull();
|
||||||
@@ -204,9 +204,7 @@ test.describe('processor detail workbench', () => {
|
|||||||
.fill('Updated before debugging');
|
.fill('Updated before debugging');
|
||||||
await basicInfoDialog.getByRole('button', { name: 'Save' }).click();
|
await basicInfoDialog.getByRole('button', { name: 'Save' }).click();
|
||||||
await expect(basicInfoDialog).toHaveCount(0);
|
await expect(basicInfoDialog).toHaveCount(0);
|
||||||
await page
|
await page.getByRole('textbox', { name: 'Message content' }).fill('Hello');
|
||||||
.getByRole('textbox', { name: 'Conversation input' })
|
|
||||||
.fill('Hello');
|
|
||||||
await page.getByRole('button', { name: 'Run test' }).click();
|
await page.getByRole('button', { name: 'Run test' }).click();
|
||||||
|
|
||||||
await expect(page.getByText('Mock Agent response')).toBeVisible();
|
await expect(page.getByText('Mock Agent response')).toBeVisible();
|
||||||
@@ -253,9 +251,7 @@ test.describe('processor detail workbench', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await page.goto('/home/agents?id=agent-workbench');
|
await page.goto('/home/agents?id=agent-workbench');
|
||||||
await page
|
await page.getByRole('textbox', { name: 'Message content' }).fill('Hello');
|
||||||
.getByRole('textbox', { name: 'Conversation input' })
|
|
||||||
.fill('Hello');
|
|
||||||
await page.getByRole('button', { name: 'Run test' }).click();
|
await page.getByRole('button', { name: 'Run test' }).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import test from 'node:test';
|
||||||
|
import ts from 'typescript';
|
||||||
|
|
||||||
|
const source = fs.readFileSync(
|
||||||
|
new URL(
|
||||||
|
'../../src/app/home/agents/components/debug-event-data.ts',
|
||||||
|
import.meta.url,
|
||||||
|
),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const module = { exports: {} };
|
||||||
|
new Function(
|
||||||
|
'exports',
|
||||||
|
ts.transpileModule(source, {
|
||||||
|
compilerOptions: {
|
||||||
|
module: ts.ModuleKind.CommonJS,
|
||||||
|
target: ts.ScriptTarget.ES2022,
|
||||||
|
},
|
||||||
|
}).outputText,
|
||||||
|
)(module.exports);
|
||||||
|
const {
|
||||||
|
createDebugEventData,
|
||||||
|
parseDebugEventData,
|
||||||
|
debugEventInputText,
|
||||||
|
invalidDebugEventField,
|
||||||
|
} = module.exports;
|
||||||
|
const samples = { user: '测试用户', message: '你好', feedback: '很有帮助' };
|
||||||
|
|
||||||
|
test('message text and common fields belong to the same event data', () => {
|
||||||
|
const data = createDebugEventData('message.received', samples);
|
||||||
|
data.text = 'A different message';
|
||||||
|
data.user_name = 'Alice';
|
||||||
|
data.custom = { nested: true };
|
||||||
|
const restored = parseDebugEventData(JSON.stringify(data));
|
||||||
|
assert.deepEqual(restored, data);
|
||||||
|
assert.equal(
|
||||||
|
debugEventInputText('message.received', restored),
|
||||||
|
'A different message',
|
||||||
|
);
|
||||||
|
assert.equal(restored.user_name, 'Alice');
|
||||||
|
assert.equal(createDebugEventData('message.received', samples).text, '你好');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-message events and deleted messages do not require fabricated conversation text', () => {
|
||||||
|
for (const event of [
|
||||||
|
'group.member_left',
|
||||||
|
'friend.request_received',
|
||||||
|
'message.deleted',
|
||||||
|
'message.reaction',
|
||||||
|
]) {
|
||||||
|
const data = createDebugEventData(event, samples);
|
||||||
|
assert.equal(invalidDebugEventField(event, data), undefined);
|
||||||
|
assert.equal(debugEventInputText(event, data), '');
|
||||||
|
assert.equal(data.text, undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('edited message uses the edited content, not an event description', () => {
|
||||||
|
const data = createDebugEventData('message.edited', samples);
|
||||||
|
data.text = 'Corrected message';
|
||||||
|
assert.equal(
|
||||||
|
debugEventInputText('message.edited', data),
|
||||||
|
'Corrected message',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
invalidDebugEventField('message.edited', { ...data, text: ' ' }).key,
|
||||||
|
'text',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid JSON cannot silently become an empty event', () => {
|
||||||
|
for (const text of ['', '{', 'null', '[]', '"hello"', '12']) {
|
||||||
|
assert.equal(parseDebugEventData(text), null);
|
||||||
|
}
|
||||||
|
const data = { arbitrary: { list: [1, true] } };
|
||||||
|
assert.deepEqual(parseDebugEventData(JSON.stringify(data)), data);
|
||||||
|
assert.equal(invalidDebugEventField('custom.example', data), undefined);
|
||||||
|
assert.equal(invalidDebugEventField('constructor', data), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('duration and rating remain numbers and reject invalid values', () => {
|
||||||
|
const data = createDebugEventData('bot.muted', samples);
|
||||||
|
assert.equal(data.duration, 60);
|
||||||
|
for (const duration of [-1, 0.5, '60']) {
|
||||||
|
assert.equal(
|
||||||
|
invalidDebugEventField('bot.muted', { ...data, duration }).key,
|
||||||
|
'duration',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert.equal(
|
||||||
|
invalidDebugEventField('bot.muted', { ...data, duration: 0 }),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
invalidDebugEventField('feedback.received', { rating: 6 }).key,
|
||||||
|
'rating',
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user