fix(acceptance): improve bot setup and execution log presentation

This commit is contained in:
Hyu
2026-10-09 17:03:15 +08:00
parent 113bb0b773
commit e22bea7f1e
29 changed files with 450 additions and 151 deletions
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Brain, ChevronRight, RefreshCw, Wrench } from 'lucide-react';
import { ChevronRight, RefreshCw } from 'lucide-react';
import AgentRunTimeline, { isReplyStep, isMockStep } from './AgentRunTimeline';
import { httpClient } from '@/app/infra/http/HttpClient';
import type {
AgentPlatformTool,
@@ -206,7 +207,12 @@ export default function AgentMonitoringTab({
const labels = Object.fromEntries(
platformTools.map((tool) => [tool.name, extractI18nObject(tool.label)]),
);
const steps = executionSteps(events);
const steps = executionSteps(events).filter(
(step) => step.kind === 'tool' || step.text.trim() || step.reasoning.trim(),
);
const toolSteps = steps.filter((step) => step.kind === 'tool');
const sentCount = toolSteps.filter((step) => isReplyStep(step) && !isMockStep(step) && step.status === 'completed').length;
const sendFailed = toolSteps.some((step) => isReplyStep(step) && step.status === 'failed');
const duration = selected ? processorRunDuration(selected) : null;
const failureReason =
selected?.status_reason ||
@@ -261,7 +267,7 @@ export default function AgentMonitoringTab({
<AlertDescription>{t('monitoring.loadError')}</AlertDescription>
</Alert>
)}
<div className="grid min-h-0 flex-1 gap-4 lg:grid-cols-[minmax(16rem,0.8fr)_minmax(0,2fr)]">
<div className="grid min-h-0 flex-1 gap-6 lg:grid-cols-[18rem_minmax(0,1fr)]">
<section
className="flex min-h-0 min-w-0 flex-col gap-2"
aria-label={t('agents.eventProcessor.runs')}
@@ -279,13 +285,14 @@ export default function AgentMonitoringTab({
variant="ghost"
aria-pressed={selectedId === run.run_id}
onClick={() => setSelectedId(run.run_id)}
className="h-auto w-full flex-col items-stretch gap-2 rounded-none border-b p-3 text-left font-normal last:border-b-0 aria-pressed:bg-accent"
className="h-auto w-full flex-col items-stretch gap-2 rounded-none border-b border-l-2 border-l-transparent p-4 text-left font-normal last:border-b-0 aria-pressed:border-l-primary aria-pressed:bg-primary/5"
>
<span className="flex items-center justify-between gap-2">
<span className="truncate font-medium">
{eventPatternLabel(run.metadata.event_type ?? '', t)}
</span>
<Badge
status={run.status}
variant={
failedStatuses.has(run.status) ? 'destructive' : 'outline'
}
@@ -303,7 +310,7 @@ export default function AgentMonitoringTab({
{new Date(run.created_at * 1000).toLocaleString()}
</time>
{debugRun(run) && (
<Badge variant="secondary">{t('agents.debugTab')}</Badge>
<Badge status="debug">{t('agents.debugTab')}</Badge>
)}
{processorRunDuration(run) !== null && (
<span>{formatRunDuration(processorRunDuration(run)!)}</span>
@@ -343,6 +350,7 @@ export default function AgentMonitoringTab({
{eventPatternLabel(selected.metadata.event_type ?? '', t)}
</h2>
<Badge
status={selected.status}
variant={
failedStatuses.has(selected.status)
? 'destructive'
@@ -352,7 +360,7 @@ export default function AgentMonitoringTab({
{stateLabel(selected)}
</Badge>
{debugRun(selected) && (
<Badge variant="secondary">{t('agents.debugTab')}</Badge>
<Badge status="debug">{t('agents.debugTab')}</Badge>
)}
</div>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
@@ -373,9 +381,22 @@ export default function AgentMonitoringTab({
</span>
)}
</div>
<p className="break-all text-xs text-muted-foreground">
{selected.runner_id}
</p>
</div>
<div className="grid grid-cols-1 divide-y rounded-lg border bg-muted/20 sm:grid-cols-3 sm:divide-x sm:divide-y-0">
<div className="space-y-1 p-4">
<p className="text-xs text-muted-foreground">{t('agents.monitoring.runStatus')}</p>
<p className="text-sm font-semibold">{stateLabel(selected)}</p>
</div>
<div className="space-y-1 p-4">
<p className="text-xs text-muted-foreground">{t('agents.monitoring.toolCalls')}</p>
<p className="text-sm font-semibold tabular-nums">{toolSteps.length}</p>
</div>
<div className="space-y-1 p-4">
<p className="text-xs text-muted-foreground">{t('agents.monitoring.delivery')}</p>
<p className={`text-sm font-semibold ${sendFailed ? 'text-destructive' : ''}`}>
{t(`agents.monitoring.${sendFailed ? 'sendFailed' : sentCount ? 'sendCount' : 'noSend'}`, { count: sentCount })}
</p>
</div>
</div>
{failedStatuses.has(selected.status) && failureReason && (
<Alert variant="destructive">
@@ -418,72 +439,14 @@ export default function AgentMonitoringTab({
</Card>
<h3 className="text-sm font-medium">
{t('agents.monitoring.execution')}
<span className="ml-2 text-muted-foreground">{steps.length}</span>
</h3>
{steps.map((step, index) => (
<Card key={index} className="gap-2 py-3">
<CardHeader className="px-3">
<CardTitle className="flex min-w-0 items-center gap-2 text-sm">
{step.kind === 'tool' ? (
<>
<Wrench className="size-4 shrink-0" />
<span className="break-all">
{labels[step.name] || step.name}
</span>
<Badge
variant={
step.status === 'failed'
? 'destructive'
: 'outline'
}
>
{t(
`agents.debugTool${step.status === 'running' ? (activeStatuses.has(selected.status) || eventCursor !== null ? 'Running' : 'Interrupted') : step.result && typeof step.result === 'object' && 'mock' in step.result && step.result.mock === true ? (step.status === 'failed' ? 'MockFailed' : 'Simulated') : step.status === 'failed' ? 'Failed' : 'Completed'}`,
)}
</Badge>
</>
) : (
<>
<Brain className="size-4" />
{t('agents.debugTextOutput')}
</>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-2 px-3">
{step.kind === 'message' ? (
<>
{step.text && (
<p className={textClass}>{step.text}</p>
)}
{step.reasoning && (
<ProcessorPayload
title={t('agents.debugReasoning')}
value={step.reasoning}
/>
)}
</>
) : (
<>
{step.error && (
<p className={`${textClass} text-destructive`}>
{step.error}
</p>
)}
<ProcessorPayload
title={t('agents.debugToolArguments')}
value={step.parameters}
/>
{step.result !== undefined && (
<ProcessorPayload
title={t('agents.debugToolResult')}
value={step.result}
/>
)}
</>
)}
</CardContent>
</Card>
))}
{!steps.length && <p className="rounded-lg border border-dashed p-6 text-sm text-muted-foreground">{t('agents.monitoring.noSteps')}</p>}
<AgentRunTimeline
steps={steps}
labels={labels}
active={activeStatuses.has(selected.status) || eventCursor !== null}
/>
{events
.filter((event) => event.type === 'processor.log')
.map((event) => (
@@ -520,6 +483,7 @@ export default function AgentMonitoringTab({
>
{json({
run_id: selected.run_id,
runner_id: selected.runner_id,
usage: selected.usage,
events,
})}
@@ -0,0 +1,73 @@
import { useTranslation } from 'react-i18next';
import { MessageSquare, Send, Wrench } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { ProcessorPayload } from './PluginProcessorTrace';
import type { ExecutionStep } from './debug-execution';
import { toolDisplay, toolSummary } from './tool-summary';
export function isReplyStep(step: ExecutionStep) {
return step.kind === 'tool' &&
toolDisplay(step.name)?.kind === 'reply';
}
export function isMockStep(step: ExecutionStep) {
return step.kind === 'tool' && !!step.result &&
typeof step.result === 'object' && 'mock' in step.result && step.result.mock === true;
}
export default function AgentRunTimeline({ steps, labels, active }: {
steps: ExecutionStep[];
labels: Record<string, string>;
active: boolean;
}) {
const { t } = useTranslation();
return (
<ol className="space-y-0">
{steps.map((step, index) => {
const reply = isReplyStep(step);
const mock = isMockStep(step);
const params = step.kind === 'tool' && step.parameters && typeof step.parameters === 'object'
? step.parameters as Record<string, unknown> : {};
const summary = toolSummary(step.kind === 'tool' ? step.name : '', params);
const replyText = reply ? summary.text : '';
const actionText = reply ? '' : summary.text;
const Icon = step.kind === 'message' ? MessageSquare : reply ? Send : Wrench;
const status = step.kind === 'tool' ? step.status : null;
const statusKey = status === 'running' ? (active ? 'Running' : 'Interrupted')
: mock ? (status === 'failed' ? 'MockFailed' : 'Simulated')
: status === 'failed' ? 'Failed' : 'Completed';
return (
<li key={step.kind === 'tool' ? step.id : `message-${index}`} className="relative flex min-w-0 gap-3 pb-5 last:pb-0">
{index < steps.length - 1 && <span className="absolute bottom-0 left-4 top-8 w-px bg-border" aria-hidden="true" />}
<span className="relative flex size-8 shrink-0 items-center justify-center rounded-full border bg-background text-xs font-medium text-muted-foreground">{index + 1}</span>
<div className="min-w-0 flex-1 rounded-lg border bg-background">
<div className="flex flex-wrap items-center gap-2 border-b bg-muted/30 px-4 py-3">
<Icon className="size-4 shrink-0 text-muted-foreground" />
<h4 className="min-w-0 flex-1 break-words text-sm font-medium">
{step.kind === 'message' ? t('agents.debugTextOutput') : labels[step.name] || step.name}
</h4>
{status && <Badge status={status === 'failed' ? 'failed' : mock ? 'simulated' : status === 'running' && !active ? 'cancelled' : status}>{t(`agents.debugTool${statusKey}`)}</Badge>}
</div>
<div className="space-y-3 p-4 text-sm">
{step.kind === 'message' ? <>
{step.text && <p className="whitespace-pre-wrap break-words leading-7 [overflow-wrap:anywhere]">{step.text}</p>}
{step.reasoning && <ProcessorPayload title={t('agents.debugReasoning')} value={step.reasoning} />}
</> : <>
{replyText && <p className="whitespace-pre-wrap break-words leading-7 [overflow-wrap:anywhere]">{replyText}</p>}
{!reply && actionText && <p className="whitespace-pre-wrap break-words rounded-md bg-muted/50 p-3 font-mono text-xs leading-6 [overflow-wrap:anywhere]">{actionText}</p>}
{summary.recipient && <p className="break-all text-xs text-muted-foreground">{t('agents.monitoring.recipient')}: {summary.recipient}</p>}
{step.error && <p role="alert" className="whitespace-pre-wrap break-words text-destructive">{step.error}</p>}
{status === 'completed' && (mock || !reply) && <p className="text-xs text-muted-foreground">{t(`agents.monitoring.${mock ? 'simulatedAction' : 'actionCompleted'}`)}</p>}
{(step.parameters != null || step.result != null) && <div className="rounded-md border border-dashed text-muted-foreground">
{step.parameters != null && <ProcessorPayload title={t('agents.debugToolArguments')} value={step.parameters} />}
{step.result != null && <ProcessorPayload title={t('agents.debugToolResult')} value={step.result} />}
</div>}
</>}
</div>
</div>
</li>
);
})}
</ol>
);
}
@@ -55,7 +55,7 @@ export default function PluginProcessorTrace({
variant={event.data.level === 'error' ? 'destructive' : 'default'}
>
<AlertDescription className="flex min-w-0 items-start gap-2">
<Badge variant="outline">{String(event.data.level)}</Badge>
<Badge status={String(event.data.level)}>{String(event.data.level)}</Badge>
<span className="min-w-0 whitespace-pre-wrap break-words [overflow-wrap:anywhere]">
{String(event.data.text)}
</span>
@@ -54,6 +54,7 @@ export default function ProcessorRunList({
</span>
<span className="flex shrink-0 flex-col items-end gap-1">
<Badge
status={run.status}
variant={
run.status === 'failed' || run.status === 'timeout'
? 'destructive'
@@ -0,0 +1,61 @@
type ToolDisplay = {
fields: readonly string[];
kind?: 'reply';
recipient?: string;
};
// Explicit display declarations for built-in tools. Unknown tools have no
// summary: their parameters remain available in the payload disclosure.
const TOOL_DISPLAYS: Readonly<Record<string, ToolDisplay>> = {
exec: { fields: ['command'] },
read: { fields: ['path'] },
write: { fields: ['path'] },
edit: { fields: ['path'] },
glob: { fields: ['pattern', 'path'] },
grep: { fields: ['pattern', 'path'] },
event_reply: { fields: ['text'], kind: 'reply' },
platform_send_message: { fields: ['text'], kind: 'reply', recipient: 'target_id' },
event_delete_message: { fields: [] },
event_get_actor: { fields: [] },
event_get_group: { fields: [] },
event_get_group_member: { fields: [] },
event_mute_member: { fields: ['duration'] },
event_unmute_member: { fields: [] },
event_kick_member: { fields: [] },
event_respond_friend_request: { fields: ['approve', 'remark'] },
event_respond_group_invite: { fields: ['approve'] },
platform_get_message: { fields: ['chat_id', 'message_id'] },
platform_delete_message: { fields: ['chat_id', 'message_id'] },
platform_get_group_info: { fields: ['group_id'] },
platform_get_group_list: { fields: [] },
platform_get_group_member_list: { fields: ['group_id'] },
platform_get_group_member_info: { fields: ['group_id', 'user_id'] },
platform_set_group_name: { fields: ['group_id', 'name'] },
platform_mute_member: { fields: ['group_id', 'user_id', 'duration'] },
platform_unmute_member: { fields: ['group_id', 'user_id'] },
platform_kick_member: { fields: ['group_id', 'user_id'] },
platform_leave_group: { fields: ['group_id'] },
platform_get_user_info: { fields: ['user_id'] },
platform_get_friend_list: { fields: [] },
};
export function toolDisplay(name: string): ToolDisplay | undefined {
return Object.prototype.hasOwnProperty.call(TOOL_DISPLAYS, name)
? TOOL_DISPLAYS[name] : undefined;
}
export function toolSummary(name: string, parameters: Record<string, unknown>) {
const display = toolDisplay(name);
const fields = (display?.fields ?? []).flatMap((field) => {
const value = parameters[field];
if (!['string', 'number', 'boolean'].includes(typeof value) || value === '') return [];
return [{ field, text: String(value) }];
});
return {
text: fields.map(({ field, text }) =>
display?.kind === 'reply' || display?.fields.length === 1 ? text : `${field}: ${text}`,
).join('\n'),
recipient: display?.recipient && typeof parameters[display.recipient] === 'string'
? String(parameters[display.recipient]) : '',
};
}
+2 -2
View File
@@ -235,11 +235,11 @@ export default function BotDetailContent({ id }: { id: string }) {
<Button
type="submit"
form="bot-form"
disabled={!formDirty}
disabled={!enableLoaded || (!formDirty && botEnabled)}
className={activeTab !== 'config' ? 'invisible' : ''}
data-guide="bot-config-save"
>
{t('common.save')}
{t(botEnabled ? 'common.save' : 'bots.saveAndEnable')}
</Button>
<Button
type="button"
@@ -32,6 +32,7 @@ import {
ChevronDown,
ChevronRight,
Webhook,
Search,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import EventBindingsEditor from './EventBindingsEditor';
@@ -305,6 +306,14 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
() => groupByCategory(activeAdapters),
[activeAdapters],
);
const [adapterSearch, setAdapterSearch] = useState('');
const adapterQuery = adapterSearch.trim().toLocaleLowerCase();
const matchesAdapterSearch = (adapter: IChooseAdapterEntity) =>
`${adapter.label} ${adapter.value} ${adapterDescriptionList[adapter.value] ?? ''}`
.toLocaleLowerCase()
.includes(adapterQuery);
const galleryGroups = groupByCategory(activeAdapters.filter(matchesAdapterSearch));
const galleryLegacyAdapters = legacyAdapters.filter(matchesAdapterSearch);
// Whether the collapsed legacy adapter group is expanded in the Select.
const [showLegacyAdapters, setShowLegacyAdapters] = useState(false);
@@ -537,6 +546,17 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
}
function selectAdapter(adapterName: string) {
if (!form.getValues('name')?.trim()) {
const selectedAdapter = adapterNameList.find(
(adapter) => adapter.value === adapterName,
);
if (selectedAdapter) {
form.setValue('name', selectedAdapter.label, {
shouldDirty: true,
shouldValidate: true,
});
}
}
form.setValue('adapter', adapterName, {
shouldDirty: !isInitializing.current,
shouldValidate: true,
@@ -664,7 +684,7 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
description: form.getValues().description ?? '',
adapter: form.getValues().adapter,
adapter_config: form.getValues().adapter_config,
enable: form.getValues().enable,
enable: true,
event_bindings: form.getValues().event_bindings ?? [],
plugin_processors: form.getValues().plugin_processors ?? [],
};
@@ -672,7 +692,7 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
.updateBot(initBotId, updateBot)
.then(() => {
// Reset dirty baseline to current values so isDirty becomes false
form.reset(form.getValues());
form.reset({ ...form.getValues(), enable: true });
onFormSubmit(form.getValues());
toast.success(t('bots.saveSuccess'));
})
@@ -755,17 +775,38 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
data-testid="adapter-gallery"
className="min-w-0 space-y-8 py-2"
>
<div className="max-w-2xl">
<h2 className="text-xl font-semibold">
{t('guidedTour.bot.adapter.title')}
</h2>
<div>
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 className="text-xl font-semibold">
{t('guidedTour.bot.adapter.title')}
</h2>
<div className="relative w-full sm:w-64">
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
type="search"
value={adapterSearch}
onChange={(event) => setAdapterSearch(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') event.preventDefault();
}}
placeholder={t('bots.searchAdapters')}
aria-label={t('bots.searchAdapters')}
className="pl-9"
/>
</div>
</div>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
{t('guidedTour.bot.adapter.description')}
</p>
</div>
<div className="space-y-8">
{groupedAdapters.map((group) => (
{galleryGroups.length === 0 && galleryLegacyAdapters.length === 0 && (
<p className="py-8 text-center text-sm text-muted-foreground" role="status">
{t('bots.noMatchingAdapters')}
</p>
)}
{galleryGroups.map((group) => (
<section
key={group.categoryId ?? 'uncategorized'}
data-adapter-category={
@@ -820,7 +861,7 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
</section>
))}
{legacyAdapters.length > 0 && (
{galleryLegacyAdapters.length > 0 && (
<section
className="border-t pt-5"
data-adapter-category="legacy"
@@ -829,21 +870,21 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
type="button"
onClick={() => setShowLegacyAdapters((value) => !value)}
className="flex w-full items-center gap-2 py-2 text-left text-sm font-medium text-muted-foreground hover:text-foreground"
aria-expanded={showLegacyAdapters}
aria-expanded={showLegacyAdapters || !!adapterQuery}
>
{showLegacyAdapters ? (
{showLegacyAdapters || adapterQuery ? (
<ChevronDown className="size-4" />
) : (
<ChevronRight className="size-4" />
)}
{t('bots.legacyAdapters')}
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px]">
{legacyAdapters.length}
{galleryLegacyAdapters.length}
</span>
</button>
{showLegacyAdapters && (
{(showLegacyAdapters || !!adapterQuery) && (
<div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{legacyAdapters.map((item) => (
{galleryLegacyAdapters.map((item) => (
<button
key={`legacy:${item.value}`}
type="button"
@@ -222,6 +222,7 @@ function AssistantSessions(props: AssistantPanelProps) {
const ASSISTANT_HEIGHT_STORAGE_KEY = 'langbot-assistant-panel-height';
const ASSISTANT_POSITION_STORAGE_KEY = 'langbot-assistant-panel-position';
const ASSISTANT_OPEN_STORAGE_KEY = 'langbot-assistant-panel-open';
function AssistantPanel({
storageKey,
@@ -235,7 +236,20 @@ function AssistantPanel({
const { t } = useTranslation();
const initialModelUuid = initialModel?.uuid;
const initialModelName = initialModel?.name;
const [open, setOpen] = useState(true);
const [open, setOpen] = useState(() => {
try {
return localStorage.getItem(ASSISTANT_OPEN_STORAGE_KEY) !== 'false';
} catch {
return true;
}
});
useEffect(() => {
try {
localStorage.setItem(ASSISTANT_OPEN_STORAGE_KEY, String(open));
} catch {
// The panel remains usable when browser storage is unavailable.
}
}, [open]);
const [closing, setClosing] = useState(false);
const [horizontalPosition, setHorizontalPosition] = useState(() => {
try {
@@ -20,14 +20,13 @@ import type {
ExecutionRow,
ExecutionSection,
} from '@/app/infra/entities/api/monitoring-executions';
import { ExecutionValue as Value } from './ExecutionValue';
import { ExecutionValue as Value, hasExecutionValue } from './ExecutionValue';
import { formatRunDuration } from '@/app/home/agents/components/processor-run-timing';
import { formatDateTime } from '../../utils/dateUtils';
import {
executionKindLabel,
executionProcessorLabel,
executionStatusLabel,
executionStatusVariant,
} from './ExecutionTable';
export type ExecutionSelection = Pick<ExecutionRow, 'id'> & {
@@ -35,6 +34,24 @@ export type ExecutionSelection = Pick<ExecutionRow, 'id'> & {
};
const mainSections: ExecutionSection[] = ['inputs', 'outputs', 'deliveries'];
// Keep transport metadata in the payload disclosure, not in the message body.
function messageBody(value: unknown): unknown {
if (typeof value === 'string') {
if (/^\s*[\[{]/.test(value)) {
try { return messageBody(JSON.parse(value)); } catch { /* Plain text. */ }
}
return value;
}
if (Array.isArray(value)) return value.map(messageBody).filter((item) => item != null && item !== '');
if (!value || typeof value !== 'object') return null;
const data = value as Record<string, unknown>;
if (['Image', 'File', 'Voice', 'At', 'AtAll', 'Quote'].includes(String(data.type))) return value;
for (const key of ['text', 'content', 'message_chain', 'message']) {
if (data[key] != null) return messageBody(data[key]);
}
return null;
}
const secondarySections: ExecutionSection[] = [
'related',
'conversation',
@@ -157,7 +174,7 @@ export default function ExecutionDetailSheet({
{executionProcessorLabel(related)} ·{' '}
{executionKindLabel(related, t)}
</span>
<Badge variant={executionStatusVariant(related.status_group)}>
<Badge status={related.status_group}>
{executionStatusLabel(related, t)}
</Badge>
</Button>
@@ -169,6 +186,11 @@ export default function ExecutionDetailSheet({
'deliveries',
'conversation',
].includes(section);
const body = section === 'inputs'
? typeof item.content === 'string' ? item.content
: item.content && typeof item.content === 'object' && 'text' in item.content
? item.content.text : null
: messageBody(item.content);
const title = content
? [
item.actor_name || item.actor_id,
@@ -188,7 +210,7 @@ export default function ExecutionDetailSheet({
item.id,
);
return (
<div className="min-w-0 space-y-2 rounded-lg border p-3">
<div className="min-w-0 space-y-3 rounded-lg border bg-background p-4">
<div className="flex flex-wrap items-center justify-between gap-2 text-xs text-muted-foreground">
<span className="break-all">{title}</span>
<span>
@@ -200,19 +222,17 @@ export default function ExecutionDetailSheet({
</span>
{item.status && (
<Badge
variant={executionStatusVariant(
['error', 'failed'].includes(item.status)
? 'failed'
: item.status,
)}
status={item.status}
>
{item.status}
{t(`monitoring.execution.status.${['success', 'delivered'].includes(item.status) ? 'completed' : item.status}`, { defaultValue: item.status })}
</Badge>
)}
</div>
{content ? (
<>
<Value value={item.content} />
<div className="text-sm leading-7">
{hasExecutionValue(body) ? <Value value={body} /> : <p className="text-muted-foreground">{t('monitoring.unified.noRecords')}</p>}
</div>
<Value value={item.attachments} />
{item.origin === 'generated' && section === 'conversation' && (
<Badge variant="outline">{t('monitoring.unified.outputs')}</Badge>
@@ -222,9 +242,12 @@ export default function ExecutionDetailSheet({
{String(item.delivery.delivery_error)}
</p>
) : null}
{Array.isArray(item.metadata?.routes) && (
<Value value={item.metadata.routes} />
)}
<details className="border-t pt-2">
<summary className="cursor-pointer text-xs text-muted-foreground">{t('monitoring.unified.payload')}</summary>
<div className="mt-3 max-h-80 overflow-auto rounded-md bg-muted/40 p-3">
<Value value={item} />
</div>
</details>
</>
) : (
<>
@@ -291,12 +314,15 @@ export default function ExecutionDetailSheet({
);
const title = `${t(`monitoring.unified.${section}`)} (${page.items.length}${page.has_more ? '+' : ''})`;
return primary ? (
<section key={section}>
<h3 className="text-sm font-semibold">{title}</h3>
<section key={section} className="space-y-3">
<h3 className="flex items-center gap-2 text-sm font-semibold">
<span className="flex size-6 items-center justify-center rounded-full bg-muted text-xs text-muted-foreground">{mainSections.indexOf(section) + 1}</span>
{title}
</h3>
{body}
</section>
) : (
<details key={section} className="rounded-lg border p-3">
<details key={section} open={section === 'errors' ? true : undefined} className={`rounded-lg border p-3 ${section === 'errors' ? 'border-red-500/25 bg-red-500/5' : ''}`}>
<summary className="cursor-pointer text-sm font-medium">
{title}
</summary>
@@ -355,14 +381,14 @@ export default function ExecutionDetailSheet({
{record && (
<>
<div className="flex flex-wrap items-center gap-2">
<Badge variant={executionStatusVariant(record.status_group)}>
<Badge status={record.status_group}>
{executionStatusLabel(record, t)}
</Badge>
<Badge variant="outline">
{executionKindLabel(record, t)}
</Badge>
{record.debug && (
<Badge variant="secondary">
<Badge status="debug">
{t('monitoring.execution.debug')}
</Badge>
)}
@@ -383,7 +409,21 @@ export default function ExecutionDetailSheet({
.filter(Boolean)
.join(' · ')}
</p>
{record.status_reason && (
<div className="grid grid-cols-3 divide-x rounded-lg border bg-muted/20">
<div className="space-y-1 p-3">
<p className="text-xs text-muted-foreground">{t('monitoring.execution.detail.duration')}</p>
<p className="text-sm font-semibold tabular-nums">{record.duration_ms != null ? formatRunDuration(record.duration_ms) : '—'}</p>
</div>
<div className="space-y-1 p-3">
<p className="text-xs text-muted-foreground">{t('monitoring.llmCalls.totalTokens')}</p>
<p className="text-sm font-semibold tabular-nums">{record.usage?.total_tokens?.toLocaleString() ?? '—'}</p>
</div>
<div className="space-y-1 p-3">
<p className="text-xs text-muted-foreground">{t('monitoring.unified.deliveries')}</p>
<p className="text-sm font-semibold tabular-nums">{visible?.pages.deliveries?.items.length ?? 0}{visible?.pages.deliveries?.has_more ? '+' : ''}</p>
</div>
</div>
{record.status_reason && !['stop', 'completed', 'success'].includes(record.status_reason) && (
<p className="rounded-md bg-muted p-3 text-sm">
{record.status_reason}
</p>
@@ -161,11 +161,11 @@ export default function ExecutionTable({
</TableCell>
<TableCell>
<div className="flex flex-wrap items-center gap-1.5">
<Badge variant={executionStatusVariant(row.status_group)}>
<Badge status={row.status_group}>
{executionStatusLabel(row, t)}
</Badge>
{row.debug && (
<Badge variant="secondary">
<Badge status="debug">
{t('monitoring.execution.debug')}
</Badge>
)}
+17 -2
View File
@@ -28,19 +28,34 @@ const badgeVariants = cva(
function Badge({
className,
variant,
status,
asChild = false,
...props
}: React.ComponentProps<'span'> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
VariantProps<typeof badgeVariants> & { asChild?: boolean; status?: string }) {
const Comp = asChild ? Slot : 'span';
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
className={cn(badgeVariants({ variant: status ? 'outline' : variant }), status && statusBadgeClass(status), className)}
{...props}
/>
);
}
function statusBadgeClass(status: string): string {
if (['completed', 'success', 'succeeded', 'delivered', 'ready', 'enabled', 'connected'].includes(status))
return 'border-emerald-500/25 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300';
if (['failed', 'error', 'timeout', 'disconnected'].includes(status))
return 'border-red-500/25 bg-red-500/10 text-red-700 dark:text-red-300';
if (['running', 'active', 'connecting'].includes(status))
return 'border-blue-500/25 bg-blue-500/10 text-blue-700 dark:text-blue-300';
if (['queued', 'pending', 'created', 'claimed', 'waiting', 'warning'].includes(status))
return 'border-amber-500/25 bg-amber-500/10 text-amber-800 dark:text-amber-300';
if (['mock', 'simulated', 'debug'].includes(status))
return 'border-violet-500/25 bg-violet-500/10 text-violet-700 dark:text-violet-300';
return 'border-border bg-muted text-muted-foreground';
}
export { Badge, badgeVariants };
+13
View File
@@ -781,6 +781,9 @@ const enUS = {
deleteConfirmation: 'Are you sure you want to delete this bot?',
platformAdapter: 'Platform/Adapter Selection',
selectAdapter: 'Select Adapter',
saveAndEnable: 'Save and enable',
searchAdapters: 'Search adapters',
noMatchingAdapters: 'No matching adapters found',
legacyAdapters: 'Legacy adapters',
legacyAdapterBadge: 'Legacy',
legacyAdaptersHint:
@@ -1126,6 +1129,16 @@ const enUS = {
},
agents: {
monitoring: {
runStatus: "Run status",
toolCalls: "Tool calls",
delivery: "Message delivery",
sendFailed: "Send failed",
sendCount: "{{count}} send calls succeeded",
noSend: "No send recorded",
noSteps: "No execution steps recorded yet.",
recipient: "Recipient",
actionCompleted: "Tool completed successfully.",
simulatedAction: "Simulated action; no real message was sent.",
description:
'Follow each task from its triggering event through model output and tool execution.',
empty:
+13
View File
@@ -797,6 +797,9 @@ const esES = {
deleteConfirmation: '¿Estás seguro de que deseas eliminar este Bot?',
platformAdapter: 'Selección de plataforma/adaptador',
selectAdapter: 'Seleccionar adaptador',
saveAndEnable: 'Guardar y activar',
searchAdapters: 'Buscar adaptadores',
noMatchingAdapters: 'No se encontraron adaptadores coincidentes',
legacyAdapters: 'Adaptadores heredados',
legacyAdapterBadge: 'Heredado',
legacyAdaptersHint:
@@ -1148,6 +1151,16 @@ const esES = {
},
agents: {
monitoring: {
runStatus: "Estado de ejecución",
toolCalls: "Llamadas a herramientas",
delivery: "Envío de mensajes",
sendFailed: "Error de envío",
sendCount: "{{count}} llamadas de envío correctas",
noSend: "Sin envíos registrados",
noSteps: "Aún no hay pasos registrados.",
recipient: "Destinatario",
actionCompleted: "Herramienta ejecutada correctamente.",
simulatedAction: "Acción simulada; no se envió ningún mensaje real.",
description:
'Consulta el evento, la salida del modelo y las herramientas de cada tarea.',
empty:
+13
View File
@@ -784,6 +784,9 @@ const jaJP = {
deleteConfirmation: '本当にこのボットを削除しますか?',
platformAdapter: 'プラットフォーム/アダプター選択',
selectAdapter: 'アダプターを選択',
saveAndEnable: '保存して有効化',
searchAdapters: 'アダプターを検索',
noMatchingAdapters: '一致するアダプターが見つかりません',
legacyAdapters: '旧式アダプター',
legacyAdapterBadge: '旧式',
legacyAdaptersHint:
@@ -1136,6 +1139,16 @@ const jaJP = {
},
agents: {
monitoring: {
runStatus: "実行状態",
toolCalls: "ツール呼び出し",
delivery: "メッセージ送信",
sendFailed: "送信失敗",
sendCount: "送信 API 呼び出し成功:{{count}} 回",
noSend: "送信記録なし",
noSteps: "実行ステップはまだありません。",
recipient: "送信先",
actionCompleted: "ツールの実行が完了しました。",
simulatedAction: "シミュレーションです。実際のメッセージは送信されていません。",
description:
'各タスクのトリガーイベント、モデル出力、ツール実行を確認します。',
empty:
+13
View File
@@ -792,6 +792,9 @@ const ruRU = {
deleteConfirmation: 'Вы уверены, что хотите удалить этого бота?',
platformAdapter: 'Выбор платформы/адаптера',
selectAdapter: 'Выберите адаптер',
saveAndEnable: 'Сохранить и включить',
searchAdapters: 'Поиск адаптеров',
noMatchingAdapters: 'Подходящие адаптеры не найдены',
legacyAdapters: 'Устаревшие адаптеры',
legacyAdapterBadge: 'Устаревший',
legacyAdaptersHint:
@@ -1146,6 +1149,16 @@ const ruRU = {
},
agents: {
monitoring: {
runStatus: "Статус выполнения",
toolCalls: "Вызовы инструментов",
delivery: "Отправка сообщений",
sendFailed: "Ошибка отправки",
sendCount: "Успешных вызовов отправки: {{count}}",
noSend: "Отправки не зарегистрированы",
noSteps: "Шаги выполнения ещё не зарегистрированы.",
recipient: "Получатель",
actionCompleted: "Инструмент выполнен успешно.",
simulatedAction: "Действие смоделировано; реальное сообщение не отправлено.",
description:
'Просмотр события, ответа модели и вызовов инструментов для каждой задачи.',
empty:
+13
View File
@@ -768,6 +768,9 @@ const thTH = {
deleteConfirmation: 'คุณแน่ใจหรือไม่ว่าต้องการลบ Bot นี้?',
platformAdapter: 'การเลือกแพลตฟอร์ม/อะแดปเตอร์',
selectAdapter: 'เลือกอะแดปเตอร์',
saveAndEnable: 'บันทึกและเปิดใช้งาน',
searchAdapters: 'ค้นหาอะแดปเตอร์',
noMatchingAdapters: 'ไม่พบอะแดปเตอร์ที่ตรงกัน',
legacyAdapters: 'อะแดปเตอร์รุ่นเก่า',
legacyAdapterBadge: 'เก่า',
legacyAdaptersHint:
@@ -1106,6 +1109,16 @@ const thTH = {
},
agents: {
monitoring: {
runStatus: "สถานะการทำงาน",
toolCalls: "การเรียกเครื่องมือ",
delivery: "การส่งข้อความ",
sendFailed: "ส่งไม่สำเร็จ",
sendCount: "เรียกส่งสำเร็จ {{count}} ครั้ง",
noSend: "ไม่มีบันทึกการส่ง",
noSteps: "ยังไม่มีบันทึกขั้นตอนการทำงาน",
recipient: "ผู้รับ",
actionCompleted: "เครื่องมือทำงานสำเร็จ",
simulatedAction: "การจำลอง ไม่ได้ส่งข้อความจริง",
description:
'ดูเหตุการณ์เริ่มต้น ผลลัพธ์โมเดล และการเรียกเครื่องมือของแต่ละงาน',
empty: 'ยังไม่มีการทำงาน เริ่มเหตุการณ์หรือทดสอบการดีบักเพื่อดูบันทึก',
+13
View File
@@ -782,6 +782,9 @@ const viVN = {
deleteConfirmation: 'Bạn có chắc chắn muốn xóa Bot này không?',
platformAdapter: 'Nền tảng/Lựa chọn Adapter',
selectAdapter: 'Chọn Adapter',
saveAndEnable: 'Lưu và bật',
searchAdapters: 'Tìm kiếm adapter',
noMatchingAdapters: 'Không tìm thấy adapter phù hợp',
legacyAdapters: 'Adapter cũ',
legacyAdapterBadge: 'Cũ',
legacyAdaptersHint:
@@ -1124,6 +1127,16 @@ const viVN = {
},
agents: {
monitoring: {
runStatus: "Trạng thái thực thi",
toolCalls: "Lượt gọi công cụ",
delivery: "Gửi tin nhắn",
sendFailed: "Gửi thất bại",
sendCount: "{{count}} lượt gọi gửi thành công",
noSend: "Chưa ghi nhận lượt gửi",
noSteps: "Chưa ghi nhận bước thực thi.",
recipient: "Người nhận",
actionCompleted: "Công cụ đã thực thi thành công.",
simulatedAction: "Thao tác mô phỏng; không gửi tin nhắn thật.",
description:
'Xem sự kiện kích hoạt, đầu ra mô hình và quá trình gọi công cụ của mỗi tác vụ.',
empty: 'Chưa có lượt chạy. Kích hoạt sự kiện hoặc chạy thử gỡ lỗi.',
+13
View File
@@ -734,6 +734,9 @@ const zhHans = {
deleteConfirmation: '你确定要删除这个机器人吗?',
platformAdapter: '平台/适配器选择',
selectAdapter: '选择适配器',
saveAndEnable: '保存并启用',
searchAdapters: '搜索适配器',
noMatchingAdapters: '未找到匹配的适配器',
legacyAdapters: '旧版适配器',
legacyAdapterBadge: '过时',
legacyAdaptersHint:
@@ -1060,6 +1063,16 @@ const zhHans = {
},
agents: {
monitoring: {
runStatus: "执行状态",
toolCalls: "工具调用",
delivery: "消息发送",
sendFailed: "发送失败",
sendCount: "{{count}} 次发送调用成功",
noSend: "暂无发送记录",
noSteps: "暂未记录执行步骤。",
recipient: "接收目标",
actionCompleted: "工具已执行完成。",
simulatedAction: "模拟执行,未发送真实消息。",
description: '查看每次任务的触发事件、模型输出和工具执行过程。',
empty: '暂无运行记录。触发平台事件或运行调试后,可在这里查看。',
input: '触发输入',
+13
View File
@@ -732,6 +732,9 @@ const zhHant = {
deleteConfirmation: '您確定要刪除這個機器人嗎?',
platformAdapter: '平台/適配器選擇',
selectAdapter: '選擇適配器',
saveAndEnable: '儲存並啟用',
searchAdapters: '搜尋適配器',
noMatchingAdapters: '找不到符合條件的適配器',
legacyAdapters: '舊版適配器',
legacyAdapterBadge: '過時',
legacyAdaptersHint:
@@ -1059,6 +1062,16 @@ const zhHant = {
},
agents: {
monitoring: {
runStatus: "執行狀態",
toolCalls: "工具呼叫",
delivery: "訊息傳送",
sendFailed: "傳送失敗",
sendCount: "{{count}} 次傳送呼叫成功",
noSend: "暫無傳送記錄",
noSteps: "尚未記錄執行步驟。",
recipient: "接收目標",
actionCompleted: "工具已執行完成。",
simulatedAction: "模擬執行,未傳送真實訊息。",
description: '查看每次任務的觸發事件、模型輸出和工具執行過程。',
empty: '尚無執行紀錄。觸發平台事件或執行除錯後,可在此查看。',
input: '觸發輸入',