mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-24 09:25:21 +08:00
fix(monitoring): unify execution records and stabilize streaming cleanup
This commit is contained in:
@@ -77,6 +77,8 @@ interface TokenStatistics {
|
||||
}
|
||||
|
||||
interface TokenMonitoringProps {
|
||||
mode?: 'all' | 'real' | 'debug';
|
||||
statusGroup?: string;
|
||||
botIds?: string[];
|
||||
pipelineIds?: string[];
|
||||
startTime?: string;
|
||||
@@ -103,6 +105,8 @@ const TOOLTIP_STYLE: React.CSSProperties = {
|
||||
};
|
||||
|
||||
export default function TokenMonitoring({
|
||||
mode,
|
||||
statusGroup,
|
||||
botIds,
|
||||
pipelineIds,
|
||||
startTime,
|
||||
@@ -123,6 +127,9 @@ export default function TokenMonitoring({
|
||||
setError(null);
|
||||
try {
|
||||
const result = await httpClient.getTokenStatistics({
|
||||
mode,
|
||||
status:
|
||||
statusGroup && statusGroup !== 'all' ? [statusGroup] : undefined,
|
||||
botId: botIds,
|
||||
pipelineId: pipelineIds,
|
||||
startTime,
|
||||
@@ -136,7 +143,16 @@ export default function TokenMonitoring({
|
||||
setLoading(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [botIdsKey, pipelineIdsKey, startTime, endTime, bucket, refreshKey]);
|
||||
}, [
|
||||
mode,
|
||||
statusGroup,
|
||||
botIdsKey,
|
||||
pipelineIdsKey,
|
||||
startTime,
|
||||
endTime,
|
||||
bucket,
|
||||
refreshKey,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -13,14 +13,14 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { backendClient } from '@/app/infra/http';
|
||||
import { backendClient, useCurrentWorkspace } from '@/app/infra/http';
|
||||
import type {
|
||||
ExecutionCallRow,
|
||||
ExecutionDetail,
|
||||
ExecutionDetailItem,
|
||||
ExecutionRow,
|
||||
ExecutionSection,
|
||||
} from '@/app/infra/entities/api/monitoring-executions';
|
||||
import AgentExecutionTrace from '@/app/home/agents/components/AgentExecutionTrace';
|
||||
import { ExecutionValue as Value } from './ExecutionValue';
|
||||
import { formatRunDuration } from '@/app/home/agents/components/processor-run-timing';
|
||||
import { formatDateTime } from '../../utils/dateUtils';
|
||||
import {
|
||||
@@ -28,329 +28,409 @@ import {
|
||||
executionProcessorLabel,
|
||||
executionStatusLabel,
|
||||
executionStatusVariant,
|
||||
executionTargetLabel,
|
||||
} from './ExecutionTable';
|
||||
|
||||
interface ExecutionDetailSheetProps {
|
||||
row: ExecutionRow | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
export type ExecutionSelection = Pick<ExecutionRow, 'id'> & {
|
||||
source: ExecutionRow['source'] | 'auto';
|
||||
};
|
||||
|
||||
/** Walk parsed JSON, collecting every `"text"` value (OpenAI-style content). */
|
||||
function collectText(value: unknown): string {
|
||||
if (typeof value === 'string') return value;
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(collectText).filter(Boolean).join(' ');
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
const record = value as Record<string, unknown>;
|
||||
if (typeof record.text === 'string') return record.text;
|
||||
return Object.values(record).map(collectText).filter(Boolean).join(' ');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function messagePreview(raw: string | null | undefined): string {
|
||||
if (!raw) return '';
|
||||
try {
|
||||
return collectText(JSON.parse(raw)).trim() || raw;
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
function callTitle(call: ExecutionCallRow, index: number): string {
|
||||
const candidates = [
|
||||
call.model_name,
|
||||
call.model,
|
||||
call.name,
|
||||
call.tool_name,
|
||||
call.function,
|
||||
call.error_type,
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (typeof candidate === 'string' && candidate) return candidate;
|
||||
}
|
||||
return typeof call.id === 'string' && call.id ? call.id : `#${index + 1}`;
|
||||
}
|
||||
const mainSections: ExecutionSection[] = ['inputs', 'outputs', 'deliveries'];
|
||||
const secondarySections: ExecutionSection[] = [
|
||||
'related',
|
||||
'conversation',
|
||||
'llm_calls',
|
||||
'tool_calls',
|
||||
'errors',
|
||||
'events',
|
||||
];
|
||||
|
||||
export default function ExecutionDetailSheet({
|
||||
row,
|
||||
onClose,
|
||||
}: ExecutionDetailSheetProps) {
|
||||
onSelect,
|
||||
}: {
|
||||
row: ExecutionSelection | null;
|
||||
onClose: () => void;
|
||||
onSelect: (row: ExecutionSelection) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const workspace = useCurrentWorkspace()?.workspace.uuid;
|
||||
const [detail, setDetail] = useState<ExecutionDetail | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const requestRef = useRef(0);
|
||||
|
||||
const [sectionError, setSectionError] = useState<ExecutionSection | null>(
|
||||
null,
|
||||
);
|
||||
const [busy, setBusy] = useState<ExecutionSection | null>(null);
|
||||
const [refresh, setRefresh] = useState(0);
|
||||
const request = useRef(0);
|
||||
const identity = `${workspace}:${row?.source}:${row?.id}`;
|
||||
const [loadedIdentity, setLoadedIdentity] = useState('');
|
||||
const visible = loadedIdentity === identity ? detail : null;
|
||||
const id = row?.id;
|
||||
const source = row?.source;
|
||||
useEffect(() => {
|
||||
if (!row) {
|
||||
setDetail(null);
|
||||
setError(false);
|
||||
const version = ++request.current;
|
||||
setDetail(null);
|
||||
setError(false);
|
||||
setSectionError(null);
|
||||
setBusy(null);
|
||||
if (!id || !source) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const requestId = requestRef.current + 1;
|
||||
requestRef.current = requestId;
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
setDetail(null);
|
||||
backendClient
|
||||
.getExecutionDetail(row.source, row.id)
|
||||
.getExecutionDetail(source, id)
|
||||
.then((result) => {
|
||||
if (requestRef.current !== requestId) return;
|
||||
if (version !== request.current) return;
|
||||
setDetail(result);
|
||||
setLoadedIdentity(identity);
|
||||
})
|
||||
.catch(() => {
|
||||
if (requestRef.current !== requestId) return;
|
||||
setError(true);
|
||||
if (version === request.current) setError(true);
|
||||
})
|
||||
.finally(() => {
|
||||
if (requestRef.current === requestId) setLoading(false);
|
||||
});
|
||||
}, [row]);
|
||||
|
||||
const isAgent =
|
||||
detail != null ? detail.source === 'agent' : row?.source === 'agent';
|
||||
|
||||
const metadata: { label: string; value: string }[] = [];
|
||||
if (row) {
|
||||
metadata.push({
|
||||
label: t('monitoring.execution.detail.executionId'),
|
||||
value: row.id,
|
||||
});
|
||||
if (row.event_id) {
|
||||
metadata.push({
|
||||
label: t('monitoring.execution.detail.eventId'),
|
||||
value: row.event_id,
|
||||
if (version === request.current) setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
request.current += 1;
|
||||
};
|
||||
}, [id, source, workspace, identity, refresh]);
|
||||
const status = visible?.row.status_group;
|
||||
useEffect(() => {
|
||||
if (!['running', 'queued'].includes(status ?? '')) return;
|
||||
// Do not discard paged history with a background refresh.
|
||||
if (
|
||||
Object.values(visible?.pages ?? {}).some((page) => page.next_offset > 100)
|
||||
)
|
||||
return;
|
||||
const timer = setInterval(() => setRefresh((value) => value + 1), 5000);
|
||||
return () => clearInterval(timer);
|
||||
}, [status, visible]);
|
||||
const loadMore = async (section: ExecutionSection) => {
|
||||
if (!visible || !row || busy) return;
|
||||
const version = request.current;
|
||||
setBusy(section);
|
||||
setSectionError(null);
|
||||
try {
|
||||
const result = await backendClient.getExecutionDetail(
|
||||
row.source,
|
||||
row.id,
|
||||
{ section, offset: visible.pages[section]?.next_offset ?? 0 },
|
||||
);
|
||||
if (version !== request.current) return;
|
||||
const next = result.pages[section];
|
||||
if (next)
|
||||
setDetail(
|
||||
(previous) =>
|
||||
previous && {
|
||||
...previous,
|
||||
pages: {
|
||||
...previous.pages,
|
||||
[section]: {
|
||||
...next,
|
||||
items: [
|
||||
...(previous.pages[section]?.items ?? []),
|
||||
...next.items,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
if (version === request.current) setSectionError(section);
|
||||
} finally {
|
||||
if (version === request.current) setBusy(null);
|
||||
}
|
||||
if (row.runner_id) {
|
||||
metadata.push({
|
||||
label: t('monitoring.execution.detail.runner'),
|
||||
value: row.runner_id,
|
||||
});
|
||||
};
|
||||
const renderItem = (section: ExecutionSection, item: ExecutionDetailItem) => {
|
||||
if (section === 'related') {
|
||||
const related = item as unknown as ExecutionRow;
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-auto w-full justify-between whitespace-normal py-3 text-left"
|
||||
onClick={() => onSelect(related)}
|
||||
>
|
||||
<span>
|
||||
{executionProcessorLabel(related)} ·{' '}
|
||||
{executionKindLabel(related, t)}
|
||||
</span>
|
||||
<Badge variant={executionStatusVariant(related.status_group)}>
|
||||
{executionStatusLabel(related, t)}
|
||||
</Badge>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
if (row.queue_name) {
|
||||
metadata.push({
|
||||
label: t('monitoring.execution.detail.queue'),
|
||||
value: row.queue_name,
|
||||
});
|
||||
}
|
||||
if (row.created_at_ms != null) {
|
||||
metadata.push({
|
||||
label: t('monitoring.execution.detail.created'),
|
||||
value: formatDateTime(new Date(row.created_at_ms)),
|
||||
});
|
||||
}
|
||||
if (row.duration_ms != null) {
|
||||
metadata.push({
|
||||
label: t('monitoring.execution.detail.duration'),
|
||||
value: formatRunDuration(row.duration_ms),
|
||||
});
|
||||
}
|
||||
if (isAgent && row.usage?.total_tokens != null) {
|
||||
metadata.push({
|
||||
label: t('monitoring.execution.detail.usage'),
|
||||
value: row.usage.total_tokens.toLocaleString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const objectHref = row
|
||||
? row.target_kind === 'agent' && row.target_id
|
||||
? `/home/agents?id=${encodeURIComponent(row.target_id)}`
|
||||
: row.target_kind === 'pipeline' && row.target_id
|
||||
? `/home/pipelines?id=${encodeURIComponent(row.target_id)}`
|
||||
const content = [
|
||||
'inputs',
|
||||
'outputs',
|
||||
'deliveries',
|
||||
'conversation',
|
||||
].includes(section);
|
||||
const title = content
|
||||
? [
|
||||
item.actor_name || item.actor_id,
|
||||
item.event_type,
|
||||
item.role &&
|
||||
t(`monitoring.unified.roles.${item.role}`, {
|
||||
defaultValue: item.role,
|
||||
}),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
: String(
|
||||
item.model_name ||
|
||||
item.tool_name ||
|
||||
item.error_type ||
|
||||
item.type ||
|
||||
item.id,
|
||||
);
|
||||
return (
|
||||
<div className="min-w-0 space-y-2 rounded-lg border p-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 text-xs text-muted-foreground">
|
||||
<span className="break-all">{title}</span>
|
||||
<span>
|
||||
{item.timestamp_ms
|
||||
? formatDateTime(new Date(item.timestamp_ms))
|
||||
: typeof item.timestamp === 'string'
|
||||
? item.timestamp
|
||||
: ''}
|
||||
</span>
|
||||
{item.status && (
|
||||
<Badge
|
||||
variant={executionStatusVariant(
|
||||
['error', 'failed'].includes(item.status)
|
||||
? 'failed'
|
||||
: item.status,
|
||||
)}
|
||||
>
|
||||
{item.status}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{content ? (
|
||||
<>
|
||||
<Value value={item.content} />
|
||||
<Value value={item.attachments} />
|
||||
{item.origin === 'generated' && section === 'conversation' && (
|
||||
<Badge variant="outline">{t('monitoring.unified.outputs')}</Badge>
|
||||
)}
|
||||
{item.delivery?.delivery_error ? (
|
||||
<p className="text-sm text-destructive">
|
||||
{String(item.delivery.delivery_error)}
|
||||
</p>
|
||||
) : null}
|
||||
{Array.isArray(item.metadata?.routes) && (
|
||||
<Value value={item.metadata.routes} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
|
||||
{typeof item.duration === 'number' && (
|
||||
<span>{formatRunDuration(item.duration)}</span>
|
||||
)}
|
||||
{typeof item.total_tokens === 'number' && (
|
||||
<span>{item.total_tokens.toLocaleString()} Token</span>
|
||||
)}
|
||||
</div>
|
||||
<Value value={item.error_message} />
|
||||
<details>
|
||||
<summary className="cursor-pointer text-xs text-muted-foreground">
|
||||
{t('monitoring.unified.payload')}
|
||||
</summary>
|
||||
<div className="mt-2">
|
||||
<Value value={section === 'events' ? item.data : item} />
|
||||
</div>
|
||||
</details>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const renderSection = (section: ExecutionSection, primary = false) => {
|
||||
const page = visible?.pages[section];
|
||||
if (
|
||||
!page ||
|
||||
(!primary && page.items.length === 0 && section !== 'conversation')
|
||||
)
|
||||
return null;
|
||||
const body = (
|
||||
<div className="mt-3 space-y-3">
|
||||
{section === 'conversation' && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('monitoring.unified.contextHint')}
|
||||
</p>
|
||||
)}
|
||||
{page.items.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('monitoring.unified.noRecords')}
|
||||
</p>
|
||||
)}
|
||||
{page.items.map((item, i) => (
|
||||
<div key={`${item.id}-${i}`}>{renderItem(section, item)}</div>
|
||||
))}
|
||||
{sectionError === section && (
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{t('monitoring.execution.detail.loadError')}
|
||||
</p>
|
||||
)}
|
||||
{page.has_more && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={busy !== null}
|
||||
onClick={() => loadMore(section)}
|
||||
>
|
||||
{t('monitoring.unified.loadMore')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
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>
|
||||
{body}
|
||||
</section>
|
||||
) : (
|
||||
<details key={section} className="rounded-lg border p-3">
|
||||
<summary className="cursor-pointer text-sm font-medium">
|
||||
{title}
|
||||
</summary>
|
||||
{body}
|
||||
</details>
|
||||
);
|
||||
};
|
||||
const record = visible?.row;
|
||||
const href = record?.target_id
|
||||
? record.target_kind === 'pipeline'
|
||||
? `/home/pipelines?id=${encodeURIComponent(record.target_id)}`
|
||||
: ['agent', 'event_processor', 'processor'].includes(record.target_kind)
|
||||
? `/home/agents?id=${encodeURIComponent(record.target_id)}`
|
||||
: null
|
||||
: null;
|
||||
|
||||
const renderCalls = (title: string, calls: ExecutionCallRow[]) => (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">{title}</h3>
|
||||
{calls.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('monitoring.execution.detail.noEvents')}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{calls.map((call, index) => {
|
||||
const duration =
|
||||
typeof call.duration === 'number' ? call.duration : null;
|
||||
return (
|
||||
<div
|
||||
key={`${title}-${call.id}-${index}`}
|
||||
className="flex items-start justify-between gap-2 rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate">{callTitle(call, index)}</div>
|
||||
{call.error_message && (
|
||||
<div className="truncate text-xs text-muted-foreground">
|
||||
{call.error_message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{call.status && (
|
||||
<Badge
|
||||
variant={executionStatusVariant(
|
||||
String(call.status).toLowerCase(),
|
||||
)}
|
||||
>
|
||||
{call.status}
|
||||
</Badge>
|
||||
)}
|
||||
<span className="w-16 text-right tabular-nums text-muted-foreground">
|
||||
{duration == null ? '—' : formatRunDuration(duration)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Sheet
|
||||
open={row != null}
|
||||
open={row !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onClose();
|
||||
}}
|
||||
>
|
||||
<SheetContent side="right" className="w-full p-0 sm:max-w-2xl">
|
||||
<SheetDescription className="sr-only">
|
||||
{t('monitoring.execution.detail.a11yDescription')}
|
||||
</SheetDescription>
|
||||
<SheetContent side="right" className="w-full p-0 sm:max-w-3xl">
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>{t('monitoring.execution.detail.title')}</SheetTitle>
|
||||
<SheetDescription>
|
||||
{t('monitoring.execution.detail.a11yDescription')}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<ScrollArea className="h-full">
|
||||
<div className="space-y-6 p-6">
|
||||
{row && (
|
||||
<div className="space-y-5 p-5 sm:p-6">
|
||||
<div className="flex items-start justify-between gap-3 pr-8">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{record
|
||||
? record.source === 'event'
|
||||
? t('monitoring.unified.unhandled')
|
||||
: executionProcessorLabel(record)
|
||||
: t('monitoring.execution.detail.title')}
|
||||
</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={loading}
|
||||
aria-label={t('monitoring.refreshData')}
|
||||
onClick={() => setRefresh((value) => value + 1)}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{loading && <Skeleton className="h-36 w-full" />}
|
||||
{error && (
|
||||
<p role="alert" className="text-destructive">
|
||||
{t('monitoring.execution.detail.loadError')}
|
||||
</p>
|
||||
)}
|
||||
{record && (
|
||||
<>
|
||||
<SheetHeader className="space-y-3">
|
||||
<SheetTitle className="pr-8 text-left">
|
||||
{executionProcessorLabel(row)}
|
||||
</SheetTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('monitoring.execution.triggeredBy', {
|
||||
trigger: executionTargetLabel(row, t),
|
||||
})}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<Badge variant="outline">
|
||||
{executionKindLabel(row, t)}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant={executionStatusVariant(record.status_group)}>
|
||||
{executionStatusLabel(record, t)}
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
{executionKindLabel(record, t)}
|
||||
</Badge>
|
||||
{record.debug && (
|
||||
<Badge variant="secondary">
|
||||
{t('monitoring.execution.debug')}
|
||||
</Badge>
|
||||
<Badge variant={executionStatusVariant(row.status_group)}>
|
||||
{executionStatusLabel(row, t)}
|
||||
</Badge>
|
||||
{row.debug && (
|
||||
<Badge variant="secondary">
|
||||
{t('monitoring.execution.debug')}
|
||||
</Badge>
|
||||
)}
|
||||
{objectHref && (
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="ml-auto h-7"
|
||||
>
|
||||
<Link to={objectHref}>
|
||||
<ExternalLink className="mr-1 h-3.5 w-3.5" />
|
||||
{t('monitoring.execution.detail.openObject')}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{metadata.map((item) => (
|
||||
<div key={item.label} className="min-w-0 space-y-0.5">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{item.label}
|
||||
</div>
|
||||
<div className="truncate text-sm font-medium">
|
||||
{item.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
{href && (
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link to={href}>
|
||||
{t('monitoring.execution.detail.openObject')}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{isAgent ? (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium">
|
||||
{t('monitoring.execution.detail.timeline')}
|
||||
</h3>
|
||||
{loading && (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-16 w-full" />
|
||||
<Skeleton className="h-16 w-full" />
|
||||
</div>
|
||||
)}
|
||||
{!loading && error && (
|
||||
<p className="text-sm text-destructive">
|
||||
{t('monitoring.execution.detail.loadError')}
|
||||
</p>
|
||||
)}
|
||||
{!loading &&
|
||||
!error &&
|
||||
detail?.source === 'agent' &&
|
||||
(detail.events.length > 0 ? (
|
||||
<AgentExecutionTrace
|
||||
events={detail.events}
|
||||
finished={row.status_group !== 'running'}
|
||||
toolLabels={{}}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('monitoring.execution.detail.noEvents')}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-5">
|
||||
{loading && (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-20 w-full" />
|
||||
<Skeleton className="h-20 w-full" />
|
||||
</div>
|
||||
)}
|
||||
{!loading && error && (
|
||||
<p className="text-sm text-destructive">
|
||||
{t('monitoring.execution.detail.loadError')}
|
||||
</p>
|
||||
)}
|
||||
{!loading && !error && detail?.source === 'pipeline' && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">
|
||||
{t('monitoring.execution.detail.query')}
|
||||
</h3>
|
||||
<p className="whitespace-pre-wrap break-words rounded-md border bg-muted/40 px-3 py-2 text-sm">
|
||||
{messagePreview(detail.message.message_content) ||
|
||||
'—'}
|
||||
</p>
|
||||
</div>
|
||||
{renderCalls(
|
||||
t('monitoring.execution.detail.modelCalls'),
|
||||
detail.llm_calls,
|
||||
)}
|
||||
{renderCalls(
|
||||
t('monitoring.execution.detail.toolCalls'),
|
||||
detail.tool_calls,
|
||||
)}
|
||||
{renderCalls(
|
||||
t('monitoring.execution.detail.errors'),
|
||||
detail.errors,
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{[
|
||||
record.bot_name,
|
||||
record.platform,
|
||||
record.user_name || record.user_id,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
</p>
|
||||
{record.status_reason && (
|
||||
<p className="rounded-md bg-muted p-3 text-sm">
|
||||
{record.status_reason}
|
||||
</p>
|
||||
)}
|
||||
{mainSections.map((section) => renderSection(section, true))}
|
||||
{secondarySections.map((section) => renderSection(section))}
|
||||
<details className="rounded-lg border p-3">
|
||||
<summary className="cursor-pointer text-sm">
|
||||
{t('monitoring.unified.metadata')}
|
||||
</summary>
|
||||
<dl className="mt-3 space-y-2 text-xs">
|
||||
{[
|
||||
[t('monitoring.execution.detail.executionId'), record.id],
|
||||
[
|
||||
t('monitoring.execution.detail.eventId'),
|
||||
record.event_id,
|
||||
],
|
||||
[
|
||||
t('monitoring.unified.conversation'),
|
||||
record.conversation_id || record.session_id,
|
||||
],
|
||||
[
|
||||
t('monitoring.execution.detail.runner'),
|
||||
record.runner_id,
|
||||
],
|
||||
[
|
||||
t('monitoring.execution.detail.created'),
|
||||
record.created_at_ms
|
||||
? formatDateTime(new Date(record.created_at_ms))
|
||||
: null,
|
||||
],
|
||||
[
|
||||
t('monitoring.execution.detail.duration'),
|
||||
record.duration_ms != null
|
||||
? formatRunDuration(record.duration_ms)
|
||||
: null,
|
||||
],
|
||||
]
|
||||
.filter(([, value]) => value)
|
||||
.map(([label, value]) => (
|
||||
<div key={label}>
|
||||
<dt className="text-muted-foreground">{label}</dt>
|
||||
<dd className="break-all">{value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</details>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -45,17 +45,26 @@ export function executionStatusVariant(
|
||||
return 'outline';
|
||||
}
|
||||
|
||||
const KIND_GROUPS = ['agent', 'pipeline', 'processor'];
|
||||
const KIND_GROUPS = [
|
||||
'agent',
|
||||
'pipeline',
|
||||
'processor',
|
||||
'event_processor',
|
||||
'event',
|
||||
];
|
||||
|
||||
export function executionKindLabel(row: ExecutionRow, t: TFunction): string {
|
||||
if (KIND_GROUPS.includes(row.target_kind)) {
|
||||
if (row.target_kind === 'event_processor')
|
||||
return t('monitoring.execution.kind.processor');
|
||||
if (row.target_kind === 'event') return t('monitoring.unified.unhandled');
|
||||
return t(`monitoring.execution.kind.${row.target_kind}`);
|
||||
}
|
||||
return row.target_kind;
|
||||
}
|
||||
|
||||
export function executionTargetLabel(row: ExecutionRow, t: TFunction): string {
|
||||
if (row.target_kind === 'agent') {
|
||||
if (row.source !== 'pipeline') {
|
||||
return eventPatternLabel(row.title, t);
|
||||
}
|
||||
const title = row.title ?? '';
|
||||
@@ -85,6 +94,7 @@ export default function ExecutionTable({
|
||||
<TableRow>
|
||||
<TableHead>{t('monitoring.execution.columns.time')}</TableHead>
|
||||
<TableHead>{t('monitoring.execution.columns.target')}</TableHead>
|
||||
<TableHead>{t('monitoring.unified.inputs')}</TableHead>
|
||||
<TableHead>{t('monitoring.execution.columns.status')}</TableHead>
|
||||
<TableHead className="text-right">
|
||||
{t('monitoring.execution.columns.duration')}
|
||||
@@ -127,7 +137,9 @@ export default function ExecutionTable({
|
||||
{executionKindLabel(row, t)}
|
||||
</Badge>
|
||||
<span className="truncate font-medium">
|
||||
{executionProcessorLabel(row)}
|
||||
{row.source === 'event'
|
||||
? t('monitoring.unified.unhandled')
|
||||
: executionProcessorLabel(row)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
@@ -137,6 +149,16 @@ export default function ExecutionTable({
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="max-w-xs">
|
||||
<div className="line-clamp-2 break-words text-sm">
|
||||
{row.input_preview || '—'}
|
||||
</div>
|
||||
<div className="mt-1 truncate text-xs text-muted-foreground">
|
||||
{[row.bot_name, row.user_name || row.user_id]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<Badge variant={executionStatusVariant(row.status_group)}>
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import { MessageContentRenderer } from '../MessageContentRenderer';
|
||||
|
||||
// Keep arbitrary event properties, including false and zero. Empty transport
|
||||
// fields are not messages. This also applies to historical serialized payloads.
|
||||
export function hasExecutionValue(value: unknown): boolean {
|
||||
if (value == null) return false;
|
||||
if (typeof value === 'string') return value.trim().length > 0;
|
||||
if (Array.isArray(value)) return value.some(hasExecutionValue);
|
||||
if (typeof value === 'object')
|
||||
return Object.values(value).some(hasExecutionValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
function textProjection(value: unknown): string {
|
||||
if (typeof value === 'string') return value;
|
||||
if (Array.isArray(value)) return value.map(textProjection).join('');
|
||||
if (value && typeof value === 'object') {
|
||||
const item = value as Record<string, unknown>;
|
||||
if (item.type === 'text' || item.type === 'Plain')
|
||||
return typeof item.text === 'string' ? item.text : '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const messageTypes = new Set([
|
||||
'Plain',
|
||||
'At',
|
||||
'AtAll',
|
||||
'Image',
|
||||
'File',
|
||||
'Voice',
|
||||
'Quote',
|
||||
]);
|
||||
const componentFields: Record<string, string[]> = {
|
||||
Plain: ['text'],
|
||||
At: ['target', 'display'],
|
||||
AtAll: [],
|
||||
Image: ['url', 'base64', 'imageId'],
|
||||
File: ['name', 'id', 'url', 'size'],
|
||||
Voice: ['url', 'base64', 'voiceId', 'length'],
|
||||
Quote: ['origin'],
|
||||
};
|
||||
|
||||
export function ExecutionValue({ value }: { value: unknown }) {
|
||||
if (!hasExecutionValue(value)) return null;
|
||||
if (typeof value === 'string') {
|
||||
// Do not reinterpret ordinary strings such as "false", "0", or quoted
|
||||
// user text. Only historical structured JSON needs decoding.
|
||||
if (/^\s*[\[{]/.test(value)) {
|
||||
try {
|
||||
return <ExecutionValue value={JSON.parse(value)} />;
|
||||
} catch {
|
||||
/* Text. */
|
||||
}
|
||||
}
|
||||
return (
|
||||
<pre className="whitespace-pre-wrap break-words font-sans text-sm [overflow-wrap:anywhere]">
|
||||
{value}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
if (Array.isArray(value))
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{value.map((item, index) => (
|
||||
<ExecutionValue key={index} value={item} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
if (typeof value === 'object') {
|
||||
const record = value as Record<string, unknown>;
|
||||
if (typeof record.type === 'string' && messageTypes.has(record.type)) {
|
||||
const fields = ['type', ...componentFields[record.type]];
|
||||
return (
|
||||
<>
|
||||
<MessageContentRenderer
|
||||
content={JSON.stringify([record])}
|
||||
maxLines={12}
|
||||
/>
|
||||
<ExecutionValue
|
||||
value={Object.fromEntries(
|
||||
Object.entries(record).filter(([key]) => !fields.includes(key)),
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (record.type === 'text') {
|
||||
const rest = Object.fromEntries(
|
||||
Object.entries(record).filter(
|
||||
([key, item]) =>
|
||||
key !== 'type' && key !== 'text' && hasExecutionValue(item),
|
||||
),
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ExecutionValue value={record.text} />
|
||||
<ExecutionValue value={rest} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (['image', 'image_url', 'image_base64'].includes(String(record.type))) {
|
||||
const image = record.image_url;
|
||||
const url =
|
||||
typeof image === 'string'
|
||||
? image
|
||||
: image && typeof image === 'object'
|
||||
? (image as { url?: string }).url
|
||||
: typeof record.image_base64 === 'string'
|
||||
? record.image_base64.startsWith('data:')
|
||||
? record.image_base64
|
||||
: `data:image/png;base64,${record.image_base64}`
|
||||
: null;
|
||||
if (url)
|
||||
return (
|
||||
<>
|
||||
<MessageContentRenderer
|
||||
content={JSON.stringify([{ type: 'Image', url }])}
|
||||
maxLines={12}
|
||||
/>
|
||||
<ExecutionValue
|
||||
value={Object.fromEntries(
|
||||
Object.entries(record).filter(
|
||||
([key]) =>
|
||||
!['type', 'image_url', 'image_base64'].includes(key),
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (['file', 'file_url', 'file_base64'].includes(String(record.type))) {
|
||||
const name = record.file_name ?? record.name;
|
||||
return (
|
||||
<>
|
||||
<MessageContentRenderer
|
||||
content={JSON.stringify([{ type: 'File', name }])}
|
||||
maxLines={12}
|
||||
/>
|
||||
<ExecutionValue
|
||||
value={Object.fromEntries(
|
||||
Object.entries(record).filter(
|
||||
([key]) =>
|
||||
!['type', 'file_name', 'name', 'file_base64'].includes(key),
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
// Only equal text projections are aliases. Never deduplicate distinct
|
||||
// records, or hide different text and structured content from one event.
|
||||
const bodyKeys = ['chain', 'message_chain', 'contents', 'content'];
|
||||
const bodyKey = bodyKeys.find((key) => hasExecutionValue(record[key]));
|
||||
const body = bodyKey ? record[bodyKey] : undefined;
|
||||
const duplicateText =
|
||||
bodyKey &&
|
||||
typeof record.text === 'string' &&
|
||||
record.text === textProjection(body);
|
||||
const entries = Object.entries(record).filter(
|
||||
([key, item]) =>
|
||||
hasExecutionValue(item) && !(key === 'text' && duplicateText),
|
||||
);
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{entries.map(([key, item]) => (
|
||||
<div key={key}>
|
||||
{!['text', ...bodyKeys, 'attachments'].includes(key) && (
|
||||
<div className="text-xs text-muted-foreground">{key}</div>
|
||||
)}
|
||||
<ExecutionValue value={item} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <span className="text-sm">{String(value)}</span>;
|
||||
}
|
||||
@@ -8,12 +8,9 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { backendClient } from '@/app/infra/http';
|
||||
import { backendClient, useCurrentWorkspace } from '@/app/infra/http';
|
||||
import { TimeRangeOption } from '../../types/monitoring';
|
||||
import type {
|
||||
ExecutionModeFilter,
|
||||
ExecutionSourceFilter,
|
||||
} from '@/app/infra/entities/api/monitoring-executions';
|
||||
import type { ExecutionModeFilter } from '@/app/infra/entities/api/monitoring-executions';
|
||||
|
||||
interface MonitoringFiltersProps {
|
||||
selectedBots: string[];
|
||||
@@ -22,8 +19,6 @@ interface MonitoringFiltersProps {
|
||||
onBotsChange: (bots: string[]) => void;
|
||||
onPipelinesChange: (pipelines: string[]) => void;
|
||||
onTimeRangeChange: (timeRange: TimeRangeOption) => void;
|
||||
source?: ExecutionSourceFilter;
|
||||
onSourceChange?: (value: ExecutionSourceFilter) => void;
|
||||
mode?: ExecutionModeFilter;
|
||||
onModeChange?: (value: ExecutionModeFilter) => void;
|
||||
statusGroup?: string;
|
||||
@@ -39,14 +34,25 @@ const STATUS_GROUP_OPTIONS = [
|
||||
'ignored',
|
||||
];
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
completed: 'text-emerald-700 dark:text-emerald-400',
|
||||
failed: 'text-red-600 dark:text-red-400',
|
||||
running: 'text-blue-600 dark:text-blue-400',
|
||||
queued: 'text-amber-700 dark:text-amber-400',
|
||||
cancelled: 'text-slate-600 dark:text-slate-400',
|
||||
ignored: 'text-muted-foreground',
|
||||
};
|
||||
|
||||
interface Bot {
|
||||
uuid: string;
|
||||
name: string;
|
||||
iconURL: string;
|
||||
}
|
||||
|
||||
interface Pipeline {
|
||||
interface Processor {
|
||||
uuid: string;
|
||||
name: string;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
export default function MonitoringFilters({
|
||||
@@ -56,21 +62,22 @@ export default function MonitoringFilters({
|
||||
onBotsChange,
|
||||
onPipelinesChange,
|
||||
onTimeRangeChange,
|
||||
source,
|
||||
onSourceChange,
|
||||
mode,
|
||||
onModeChange,
|
||||
statusGroup,
|
||||
onStatusGroupChange,
|
||||
}: MonitoringFiltersProps) {
|
||||
const { t } = useTranslation();
|
||||
const workspace = useCurrentWorkspace()?.workspace.uuid;
|
||||
const [bots, setBots] = useState<Bot[]>([]);
|
||||
const [pipelines, setPipelines] = useState<Pipeline[]>([]);
|
||||
const [processors, setProcessors] = useState<Processor[]>([]);
|
||||
const [loadingBots, setLoadingBots] = useState(false);
|
||||
const [loadingPipelines, setLoadingPipelines] = useState(false);
|
||||
const [loadingProcessors, setLoadingProcessors] = useState(false);
|
||||
|
||||
// Fetch bots list
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
setBots([]);
|
||||
const fetchBots = async () => {
|
||||
setLoadingBots(true);
|
||||
try {
|
||||
@@ -78,41 +85,57 @@ export default function MonitoringFilters({
|
||||
// Filter out bots without uuid and map to local Bot interface
|
||||
const validBots = (response.bots || [])
|
||||
.filter((bot): bot is typeof bot & { uuid: string } => !!bot.uuid)
|
||||
.map((bot) => ({ uuid: bot.uuid, name: bot.name }));
|
||||
setBots(validBots);
|
||||
.map((bot) => ({
|
||||
uuid: bot.uuid,
|
||||
name: bot.name,
|
||||
iconURL: backendClient.getAdapterIconURL(bot.adapter),
|
||||
}));
|
||||
if (active) setBots(validBots);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch bots:', error);
|
||||
} finally {
|
||||
setLoadingBots(false);
|
||||
if (active) setLoadingBots(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchBots();
|
||||
}, []);
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [workspace]);
|
||||
|
||||
// Fetch pipelines list
|
||||
// Fetch all processor kinds from the same catalog as the sidebar
|
||||
useEffect(() => {
|
||||
const fetchPipelines = async () => {
|
||||
setLoadingPipelines(true);
|
||||
let active = true;
|
||||
setProcessors([]);
|
||||
const fetchProcessors = async () => {
|
||||
setLoadingProcessors(true);
|
||||
try {
|
||||
const response = await backendClient.getPipelines();
|
||||
// Filter out pipelines without uuid and map to local Pipeline interface
|
||||
const validPipelines = (response.pipelines || [])
|
||||
const response = await backendClient.getAgents();
|
||||
// Preserve the custom icon displayed to the left of the sidebar name.
|
||||
const validProcessors = (response.agents || [])
|
||||
.filter(
|
||||
(pipeline): pipeline is typeof pipeline & { uuid: string } =>
|
||||
!!pipeline.uuid,
|
||||
)
|
||||
.map((pipeline) => ({ uuid: pipeline.uuid, name: pipeline.name }));
|
||||
setPipelines(validPipelines);
|
||||
.map((pipeline) => ({
|
||||
uuid: pipeline.uuid,
|
||||
name: pipeline.name,
|
||||
emoji: pipeline.emoji,
|
||||
}));
|
||||
if (active) setProcessors(validProcessors);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch pipelines:', error);
|
||||
console.error('Failed to fetch processors:', error);
|
||||
} finally {
|
||||
setLoadingPipelines(false);
|
||||
if (active) setLoadingProcessors(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPipelines();
|
||||
}, []);
|
||||
fetchProcessors();
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [workspace]);
|
||||
|
||||
const handleBotChange = (value: string) => {
|
||||
if (value === 'all') {
|
||||
@@ -135,41 +158,104 @@ export default function MonitoringFilters({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-3 sm:w-auto sm:flex-row sm:flex-wrap sm:items-center sm:gap-6">
|
||||
{/* Source Filter */}
|
||||
{onSourceChange && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="w-20 shrink-0 text-sm font-medium sm:w-auto sm:whitespace-nowrap">
|
||||
{t('monitoring.execution.filters.source')}
|
||||
</Label>
|
||||
<Select
|
||||
value={source ?? 'all'}
|
||||
onValueChange={(value) =>
|
||||
onSourceChange(value as ExecutionSourceFilter)
|
||||
}
|
||||
<div className="grid min-w-0 flex-1 grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-5">
|
||||
{/* Bot Filter */}
|
||||
<div className="min-w-0 space-y-1.5">
|
||||
<Label
|
||||
htmlFor="monitoring-filter-bot"
|
||||
className="text-xs font-medium text-muted-foreground"
|
||||
>
|
||||
{t('monitoring.filters.bot')}
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedBots.length === 0 ? 'all' : selectedBots[0]}
|
||||
onValueChange={handleBotChange}
|
||||
disabled={loadingBots}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="monitoring-filter-bot"
|
||||
className="h-9 w-full min-w-0"
|
||||
>
|
||||
<SelectTrigger className="h-9 w-full sm:w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
{t('monitoring.execution.filters.sourceAll')}
|
||||
<SelectValue
|
||||
placeholder={
|
||||
loadingBots
|
||||
? t('monitoring.filters.loading')
|
||||
: t('monitoring.filters.selectBot')
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
{t('monitoring.filters.allBots')}
|
||||
</SelectItem>
|
||||
{bots.map((bot) => (
|
||||
<SelectItem key={bot.uuid} value={bot.uuid}>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<img
|
||||
src={bot.iconURL}
|
||||
alt=""
|
||||
className="size-4 shrink-0 rounded"
|
||||
/>
|
||||
<span className="truncate">{bot.name}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="agent">
|
||||
{t('monitoring.execution.filters.sourceAgent')}
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Processor Filter */}
|
||||
<div className="min-w-0 space-y-1.5">
|
||||
<Label
|
||||
htmlFor="monitoring-filter-processor"
|
||||
className="text-xs font-medium text-muted-foreground"
|
||||
>
|
||||
{t('monitoring.filters.processor')}
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedPipelines.length === 0 ? 'all' : selectedPipelines[0]}
|
||||
onValueChange={handlePipelineChange}
|
||||
disabled={loadingProcessors}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="monitoring-filter-processor"
|
||||
className="h-9 w-full min-w-0"
|
||||
>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
loadingProcessors
|
||||
? t('monitoring.filters.loading')
|
||||
: t('monitoring.filters.selectProcessor')
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
{t('monitoring.filters.allProcessors')}
|
||||
</SelectItem>
|
||||
{processors.map((pipeline) => (
|
||||
<SelectItem key={pipeline.uuid} value={pipeline.uuid}>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
{pipeline.emoji && (
|
||||
<span className="shrink-0 text-sm" aria-hidden="true">
|
||||
{pipeline.emoji}
|
||||
</span>
|
||||
)}
|
||||
<span className="truncate">{pipeline.name}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="pipeline">
|
||||
{t('monitoring.execution.filters.sourcePipeline')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Mode Filter */}
|
||||
{onModeChange && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="w-20 shrink-0 text-sm font-medium sm:w-auto sm:whitespace-nowrap">
|
||||
<div className="min-w-0 space-y-1.5">
|
||||
<Label
|
||||
htmlFor="monitoring-filter-mode"
|
||||
className="text-xs font-medium text-muted-foreground"
|
||||
>
|
||||
{t('monitoring.execution.filters.mode')}
|
||||
</Label>
|
||||
<Select
|
||||
@@ -178,7 +264,10 @@ export default function MonitoringFilters({
|
||||
onModeChange(value as ExecutionModeFilter)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-9 w-full sm:w-[140px]">
|
||||
<SelectTrigger
|
||||
id="monitoring-filter-mode"
|
||||
className="h-9 w-full min-w-0"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -198,15 +287,21 @@ export default function MonitoringFilters({
|
||||
|
||||
{/* Status Filter */}
|
||||
{onStatusGroupChange && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="w-20 shrink-0 text-sm font-medium sm:w-auto sm:whitespace-nowrap">
|
||||
<div className="min-w-0 space-y-1.5">
|
||||
<Label
|
||||
htmlFor="monitoring-filter-status"
|
||||
className="text-xs font-medium text-muted-foreground"
|
||||
>
|
||||
{t('monitoring.execution.filters.status')}
|
||||
</Label>
|
||||
<Select
|
||||
value={statusGroup ?? 'all'}
|
||||
onValueChange={(value) => onStatusGroupChange(value)}
|
||||
>
|
||||
<SelectTrigger className="h-9 w-full sm:w-[140px]">
|
||||
<SelectTrigger
|
||||
id="monitoring-filter-status"
|
||||
className="h-9 w-full min-w-0"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -215,7 +310,15 @@ export default function MonitoringFilters({
|
||||
</SelectItem>
|
||||
{STATUS_GROUP_OPTIONS.map((group) => (
|
||||
<SelectItem key={group} value={group}>
|
||||
{t(`monitoring.execution.status.${group}`)}
|
||||
<span
|
||||
className={`flex items-center gap-2 ${STATUS_COLORS[group]}`}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="size-2 shrink-0 rounded-full bg-current"
|
||||
/>
|
||||
{t(`monitoring.execution.status.${group}`)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -223,77 +326,19 @@ export default function MonitoringFilters({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bot Filter */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="w-20 shrink-0 text-sm font-medium sm:w-auto sm:whitespace-nowrap">
|
||||
{t('monitoring.filters.bot')}
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedBots.length === 0 ? 'all' : selectedBots[0]}
|
||||
onValueChange={handleBotChange}
|
||||
disabled={loadingBots}
|
||||
>
|
||||
<SelectTrigger className="h-9 w-full sm:w-[140px]">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
loadingBots
|
||||
? t('monitoring.filters.loading')
|
||||
: t('monitoring.filters.selectBot')
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
{t('monitoring.filters.allBots')}
|
||||
</SelectItem>
|
||||
{bots.map((bot) => (
|
||||
<SelectItem key={bot.uuid} value={bot.uuid}>
|
||||
{bot.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Pipeline Filter */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="w-20 shrink-0 text-sm font-medium sm:w-auto sm:whitespace-nowrap">
|
||||
{t('monitoring.filters.pipeline')}
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedPipelines.length === 0 ? 'all' : selectedPipelines[0]}
|
||||
onValueChange={handlePipelineChange}
|
||||
disabled={loadingPipelines}
|
||||
>
|
||||
<SelectTrigger className="h-9 w-full sm:w-[140px]">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
loadingPipelines
|
||||
? t('monitoring.filters.loading')
|
||||
: t('monitoring.filters.selectPipeline')
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
{t('monitoring.filters.allPipelines')}
|
||||
</SelectItem>
|
||||
{pipelines.map((pipeline) => (
|
||||
<SelectItem key={pipeline.uuid} value={pipeline.uuid}>
|
||||
{pipeline.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Time Range Filter */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="w-20 shrink-0 text-sm font-medium sm:w-auto sm:whitespace-nowrap">
|
||||
<div className="min-w-0 space-y-1.5">
|
||||
<Label
|
||||
htmlFor="monitoring-filter-time"
|
||||
className="text-xs font-medium text-muted-foreground"
|
||||
>
|
||||
{t('monitoring.filters.timeRange')}
|
||||
</Label>
|
||||
<Select value={timeRange} onValueChange={handleTimeRangeChange}>
|
||||
<SelectTrigger className="h-9 w-full sm:w-[150px]">
|
||||
<SelectTrigger
|
||||
id="monitoring-filter-time"
|
||||
className="h-9 w-full min-w-0"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -2,19 +2,12 @@ import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Activity,
|
||||
CheckCircle2,
|
||||
Info,
|
||||
Timer,
|
||||
TriangleAlert,
|
||||
Users,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import type { ExecutionSummary } from '@/app/infra/entities/api/monitoring-executions';
|
||||
import { formatRunDuration } from '@/app/home/agents/components/processor-run-timing';
|
||||
import { MetricCard } from './MetricCard';
|
||||
@@ -63,7 +56,7 @@ export default function ExecutionOverviewCards({
|
||||
>
|
||||
{!!overview?.debug && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('monitoring.execution.cards.hiddenDebug', {
|
||||
{t('monitoring.execution.cards.debugRuns', {
|
||||
count: overview.debug,
|
||||
})}
|
||||
</div>
|
||||
@@ -72,6 +65,7 @@ export default function ExecutionOverviewCards({
|
||||
|
||||
<MetricCard
|
||||
label={t('monitoring.execution.cards.successRate')}
|
||||
description={t('monitoring.execution.cards.successRateHint')}
|
||||
icon={<CheckCircle2 className="h-4 w-4" />}
|
||||
accent="#3b82f6"
|
||||
loading={loading}
|
||||
@@ -82,24 +76,6 @@ export default function ExecutionOverviewCards({
|
||||
completed: (overview?.completed ?? 0).toLocaleString(),
|
||||
denominator: (overview?.denominator ?? 0).toLocaleString(),
|
||||
})}
|
||||
action={
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-foreground"
|
||||
aria-label={t('monitoring.execution.cards.successRate')}
|
||||
>
|
||||
<Info className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
{t('monitoring.execution.cards.successRateHint')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
|
||||
<MetricCard
|
||||
@@ -142,6 +118,7 @@ export default function ExecutionOverviewCards({
|
||||
|
||||
<MetricCard
|
||||
label={t('monitoring.execution.cards.latency')}
|
||||
description={t('monitoring.execution.cards.latencyHint')}
|
||||
icon={<Timer className="h-4 w-4" />}
|
||||
accent="#f59e0b"
|
||||
loading={loading}
|
||||
@@ -175,6 +152,7 @@ export default function ExecutionOverviewCards({
|
||||
|
||||
<MetricCard
|
||||
label={t('monitoring.execution.cards.tokens')}
|
||||
description={t('monitoring.execution.cards.tokensHint')}
|
||||
icon={<Zap className="h-4 w-4" />}
|
||||
accent="#10b981"
|
||||
loading={loading}
|
||||
|
||||
@@ -4,10 +4,18 @@ import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Info } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
export interface MetricCardProps {
|
||||
/** Small label, sitting next to the icon. */
|
||||
label: string;
|
||||
description?: string;
|
||||
/** The card's headline value; a node so it can carry a badge. */
|
||||
value?: React.ReactNode;
|
||||
/** Icon of the tinted tile on the leading edge. */
|
||||
@@ -32,6 +40,7 @@ export interface MetricCardProps {
|
||||
*/
|
||||
export function MetricCard({
|
||||
label,
|
||||
description,
|
||||
value,
|
||||
icon,
|
||||
hint,
|
||||
@@ -56,9 +65,28 @@ export function MetricCard({
|
||||
{icon}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="truncate text-sm text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">{label}</span>
|
||||
{description && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 shrink-0 text-muted-foreground hover:text-foreground"
|
||||
aria-label={label}
|
||||
>
|
||||
<Info className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
sideOffset={6}
|
||||
className="max-w-[min(20rem,calc(100vw-2rem))] text-sm leading-relaxed"
|
||||
>
|
||||
{description}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { FeedbackRecord, FeedbackStats } from '../types/monitoring';
|
||||
import { parseUTCTimestamp } from '../utils/dateUtils';
|
||||
|
||||
interface UseFeedbackDataParams {
|
||||
mode?: 'all' | 'real' | 'debug';
|
||||
statusGroup?: string;
|
||||
botIds?: string[];
|
||||
pipelineIds?: string[];
|
||||
startTime?: string;
|
||||
@@ -60,6 +62,9 @@ export function useFeedbackData(params: UseFeedbackDataParams = {}) {
|
||||
const fetchStats = useCallback(async () => {
|
||||
try {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params.mode) queryParams.append('mode', params.mode);
|
||||
if (params.statusGroup && params.statusGroup !== 'all')
|
||||
queryParams.append('status', params.statusGroup);
|
||||
if (params.botIds) {
|
||||
params.botIds.forEach((id) => queryParams.append('botId', id));
|
||||
}
|
||||
@@ -99,7 +104,14 @@ export function useFeedbackData(params: UseFeedbackDataParams = {}) {
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch feedback stats:', err);
|
||||
}
|
||||
}, [params.botIds, params.pipelineIds, params.startTime, params.endTime]);
|
||||
}, [
|
||||
params.mode,
|
||||
params.statusGroup,
|
||||
params.botIds,
|
||||
params.pipelineIds,
|
||||
params.startTime,
|
||||
params.endTime,
|
||||
]);
|
||||
|
||||
const fetchFeedback = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -107,6 +119,9 @@ export function useFeedbackData(params: UseFeedbackDataParams = {}) {
|
||||
|
||||
try {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params.mode) queryParams.append('mode', params.mode);
|
||||
if (params.statusGroup && params.statusGroup !== 'all')
|
||||
queryParams.append('status', params.statusGroup);
|
||||
if (params.botIds) {
|
||||
params.botIds.forEach((id) => queryParams.append('botId', id));
|
||||
}
|
||||
|
||||
@@ -54,6 +54,11 @@ export function useMonitoringData(filterState: FilterState) {
|
||||
);
|
||||
|
||||
const response = await backendClient.getMonitoringData({
|
||||
mode: filterState.mode,
|
||||
status:
|
||||
filterState.statusGroup && filterState.statusGroup !== 'all'
|
||||
? [filterState.statusGroup]
|
||||
: undefined,
|
||||
botId:
|
||||
filterState.selectedBots.length > 0
|
||||
? filterState.selectedBots
|
||||
@@ -390,6 +395,8 @@ export function useMonitoringData(filterState: FilterState) {
|
||||
if (isCurrent()) setLoading(false);
|
||||
}
|
||||
}, [
|
||||
filterState.mode,
|
||||
filterState.statusGroup,
|
||||
filterState.timeRange,
|
||||
filterState.customDateRange,
|
||||
filterState.selectedBots,
|
||||
@@ -408,6 +415,8 @@ export function useMonitoringData(filterState: FilterState) {
|
||||
}, [
|
||||
selectedBotsStr,
|
||||
selectedPipelinesStr,
|
||||
filterState.mode,
|
||||
filterState.statusGroup,
|
||||
filterState.timeRange,
|
||||
customDateRangeStr,
|
||||
workspaceUuid,
|
||||
|
||||
@@ -1,65 +1,128 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useCurrentWorkspace } from '@/app/infra/http';
|
||||
import { FilterState, TimeRangeOption, DateRange } from '../types/monitoring';
|
||||
import { getPresetDateRange } from '../utils/dateUtils';
|
||||
import type { ExecutionModeFilter } from '@/app/infra/entities/api/monitoring-executions';
|
||||
|
||||
/**
|
||||
* Custom hook for managing monitoring filters
|
||||
*/
|
||||
export function useMonitoringFilters() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const defaults = (): FilterState => ({
|
||||
selectedBots: [],
|
||||
selectedPipelines: [],
|
||||
timeRange: 'last24Hours',
|
||||
customDateRange: null,
|
||||
mode: 'all',
|
||||
statusGroup: 'all',
|
||||
});
|
||||
const timeRanges = [
|
||||
'lastHour',
|
||||
'last6Hours',
|
||||
'last24Hours',
|
||||
'last7Days',
|
||||
'last30Days',
|
||||
'custom',
|
||||
];
|
||||
const statuses = [
|
||||
'all',
|
||||
'completed',
|
||||
'failed',
|
||||
'running',
|
||||
'queued',
|
||||
'cancelled',
|
||||
'ignored',
|
||||
];
|
||||
|
||||
// Initialize filters from URL params
|
||||
const [selectedBots, setSelectedBots] = useState<string[]>(() => {
|
||||
const botId = searchParams.get('botId');
|
||||
return botId ? [botId] : [];
|
||||
});
|
||||
|
||||
const [selectedPipelines, setSelectedPipelines] = useState<string[]>(() => {
|
||||
const pipelineId = searchParams.get('pipelineId');
|
||||
return pipelineId ? [pipelineId] : [];
|
||||
});
|
||||
|
||||
const [timeRange, setTimeRange] = useState<TimeRangeOption>('last24Hours');
|
||||
const [customDateRange, setCustomDateRange] = useState<DateRange | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
// Get the active date range (either preset or custom)
|
||||
const getActiveDateRange = (): DateRange | null => {
|
||||
if (timeRange === 'custom' && customDateRange) {
|
||||
return customDateRange;
|
||||
function restore(key: string): FilterState {
|
||||
const state = defaults();
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(key) || 'null');
|
||||
if (!saved || typeof saved !== 'object') return state;
|
||||
for (const name of ['selectedBots', 'selectedPipelines'] as const) {
|
||||
if (Array.isArray(saved[name]))
|
||||
state[name] = saved[name].filter(
|
||||
(id: unknown) => typeof id === 'string' && id.length > 0,
|
||||
);
|
||||
}
|
||||
return getPresetDateRange(timeRange);
|
||||
};
|
||||
if (timeRanges.includes(saved.timeRange)) state.timeRange = saved.timeRange;
|
||||
if (['all', 'real', 'debug'].includes(saved.mode)) state.mode = saved.mode;
|
||||
if (statuses.includes(saved.statusGroup))
|
||||
state.statusGroup = saved.statusGroup;
|
||||
if (saved.customDateRange) {
|
||||
const from = new Date(saved.customDateRange.from);
|
||||
const to = new Date(saved.customDateRange.to);
|
||||
if (
|
||||
Number.isFinite(from.getTime()) &&
|
||||
Number.isFinite(to.getTime()) &&
|
||||
from <= to
|
||||
)
|
||||
state.customDateRange = { from, to };
|
||||
}
|
||||
if (state.timeRange === 'custom' && !state.customDateRange)
|
||||
state.timeRange = 'last24Hours';
|
||||
} catch {
|
||||
// Invalid or unavailable browser storage must not prevent monitoring.
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
// Reset all filters
|
||||
/** The page is keyed by Workspace so saved resource IDs never cross scopes. */
|
||||
export function useMonitoringFilters() {
|
||||
const workspace = useCurrentWorkspace()?.workspace.uuid;
|
||||
const key = `monitoring.filters.v1:${workspace ?? 'pending'}`;
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [filterState, setFilters] = useState<FilterState>(() => {
|
||||
const state = workspace ? restore(key) : defaults();
|
||||
// Explicit links take precedence over saved resource filters.
|
||||
if (searchParams.has('botId'))
|
||||
state.selectedBots = searchParams.getAll('botId').filter(Boolean);
|
||||
if (searchParams.has('pipelineId'))
|
||||
state.selectedPipelines = searchParams
|
||||
.getAll('pipelineId')
|
||||
.filter(Boolean);
|
||||
return state;
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!workspace) return;
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(filterState));
|
||||
} catch {
|
||||
/* Storage may be disabled. */
|
||||
}
|
||||
}, [key, workspace, filterState]);
|
||||
const update = (value: Partial<FilterState>) =>
|
||||
setFilters((current) => ({ ...current, ...value }));
|
||||
const clearLinkedFilter = (name: string) => {
|
||||
if (!searchParams.has(name)) return;
|
||||
const next = new URLSearchParams(searchParams);
|
||||
next.delete(name);
|
||||
setSearchParams(next, { replace: true });
|
||||
};
|
||||
const resetFilters = () => {
|
||||
setSelectedBots([]);
|
||||
setSelectedPipelines([]);
|
||||
setTimeRange('last24Hours');
|
||||
setCustomDateRange(null);
|
||||
setFilters(defaults());
|
||||
const next = new URLSearchParams(searchParams);
|
||||
next.delete('botId');
|
||||
next.delete('pipelineId');
|
||||
setSearchParams(next, { replace: true });
|
||||
};
|
||||
|
||||
// Get the current filter state
|
||||
const filterState: FilterState = {
|
||||
selectedBots,
|
||||
selectedPipelines,
|
||||
timeRange,
|
||||
customDateRange,
|
||||
};
|
||||
|
||||
return {
|
||||
selectedBots,
|
||||
setSelectedBots,
|
||||
selectedPipelines,
|
||||
setSelectedPipelines,
|
||||
timeRange,
|
||||
setTimeRange,
|
||||
customDateRange,
|
||||
setCustomDateRange,
|
||||
getActiveDateRange,
|
||||
resetFilters,
|
||||
...filterState,
|
||||
filterState,
|
||||
setSelectedBots: (selectedBots: string[]) => {
|
||||
update({ selectedBots });
|
||||
clearLinkedFilter('botId');
|
||||
},
|
||||
setSelectedPipelines: (selectedPipelines: string[]) => {
|
||||
update({ selectedPipelines });
|
||||
clearLinkedFilter('pipelineId');
|
||||
},
|
||||
setTimeRange: (timeRange: TimeRangeOption) => update({ timeRange }),
|
||||
setCustomDateRange: (customDateRange: DateRange | null) =>
|
||||
update({ customDateRange }),
|
||||
setExecutionMode: (mode: ExecutionModeFilter) => update({ mode }),
|
||||
setExecutionStatus: (statusGroup: string) => update({ statusGroup }),
|
||||
getActiveDateRange: () =>
|
||||
filterState.timeRange === 'custom'
|
||||
? filterState.customDateRange
|
||||
: getPresetDateRange(filterState.timeRange),
|
||||
resetFilters,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
ChevronDown,
|
||||
ExternalLink,
|
||||
RefreshCw,
|
||||
MessageSquare,
|
||||
RotateCcw,
|
||||
Sparkles,
|
||||
CheckCircle2,
|
||||
Activity,
|
||||
@@ -30,20 +30,14 @@ import { ExportDropdown } from './components/ExportDropdown';
|
||||
import { useMonitoringFilters } from './hooks/useMonitoringFilters';
|
||||
import { useMonitoringData } from './hooks/useMonitoringData';
|
||||
import { useFeedbackData } from './hooks/useFeedbackData';
|
||||
import { ConversationTurnList } from './components/ConversationTurnList';
|
||||
import { FeedbackStatsCards } from './components/FeedbackCard';
|
||||
import { FeedbackList } from './components/FeedbackList';
|
||||
import ExecutionOverviewCards from './components/overview-cards/ExecutionOverviewCards';
|
||||
import ExecutionTable from './components/executions/ExecutionTable';
|
||||
import ExecutionDetailSheet from './components/executions/ExecutionDetailSheet';
|
||||
import ExecutionDetailSheet, {
|
||||
type ExecutionSelection,
|
||||
} from './components/executions/ExecutionDetailSheet';
|
||||
import { useExecutions } from './hooks/useExecutions';
|
||||
import type {
|
||||
ExecutionModeFilter,
|
||||
ExecutionRow,
|
||||
ExecutionSourceFilter,
|
||||
} from '@/app/infra/entities/api/monitoring-executions';
|
||||
import { buildConversationTurns } from './utils/conversationTurns';
|
||||
import { resolveCallTarget } from './utils/callLinks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { resolveMonitoringWindow } from './utils/dateUtils';
|
||||
import { LoadingPage } from '@/components/ui/loading-spinner';
|
||||
@@ -54,20 +48,25 @@ function MonitoringPageContent() {
|
||||
const currentWorkspace = useCurrentWorkspace();
|
||||
const canExport =
|
||||
currentWorkspace?.permissions.includes('data.export') ?? false;
|
||||
const { filterState, setSelectedBots, setSelectedPipelines, setTimeRange } =
|
||||
useMonitoringFilters();
|
||||
const { data, loading, error, refetch } = useMonitoringData(filterState);
|
||||
|
||||
// Unified execution view (agent runs + pipeline queries). Its filters are
|
||||
// independent of the legacy message/model tabs.
|
||||
const [executionSource, setExecutionSource] =
|
||||
useState<ExecutionSourceFilter>('all');
|
||||
const [executionMode, setExecutionMode] =
|
||||
useState<ExecutionModeFilter>('all');
|
||||
const [executionStatus, setExecutionStatus] = useState<string>('all');
|
||||
const {
|
||||
filterState,
|
||||
setSelectedBots,
|
||||
setSelectedPipelines,
|
||||
setTimeRange,
|
||||
setExecutionMode,
|
||||
setExecutionStatus,
|
||||
resetFilters,
|
||||
} = useMonitoringFilters();
|
||||
const executionMode = filterState.mode ?? 'all';
|
||||
const executionStatus = filterState.statusGroup ?? 'all';
|
||||
const { data, loading, error, refetch } = useMonitoringData({
|
||||
...filterState,
|
||||
mode: executionMode,
|
||||
statusGroup: executionStatus,
|
||||
});
|
||||
const [executionOffset, setExecutionOffset] = useState(0);
|
||||
const [selectedExecution, setSelectedExecution] =
|
||||
useState<ExecutionRow | null>(null);
|
||||
useState<ExecutionSelection | null>(null);
|
||||
const EXECUTIONS_PAGE_SIZE = 50;
|
||||
|
||||
const {
|
||||
@@ -79,7 +78,7 @@ function MonitoringPageContent() {
|
||||
selectedBots: filterState.selectedBots,
|
||||
selectedPipelines: filterState.selectedPipelines,
|
||||
selectedAgents: [],
|
||||
source: executionSource,
|
||||
source: 'all',
|
||||
mode: executionMode,
|
||||
statusGroup: executionStatus,
|
||||
timeRange: filterState.timeRange,
|
||||
@@ -92,7 +91,6 @@ function MonitoringPageContent() {
|
||||
useEffect(() => {
|
||||
setExecutionOffset(0);
|
||||
}, [
|
||||
executionSource,
|
||||
executionMode,
|
||||
executionStatus,
|
||||
filterState.selectedBots,
|
||||
@@ -121,6 +119,8 @@ function MonitoringPageContent() {
|
||||
stats: feedbackStats,
|
||||
loading: feedbackLoading,
|
||||
} = useFeedbackData({
|
||||
mode: executionMode,
|
||||
statusGroup: executionStatus,
|
||||
botIds:
|
||||
filterState.selectedBots.length > 0
|
||||
? filterState.selectedBots
|
||||
@@ -141,44 +141,16 @@ function MonitoringPageContent() {
|
||||
setFeedbackRefreshKey((k) => k + 1);
|
||||
}, [refetch, refetchExecutions]);
|
||||
|
||||
const conversationTurns = useMemo(
|
||||
() =>
|
||||
buildConversationTurns(
|
||||
data?.messages || [],
|
||||
data?.llmCalls || [],
|
||||
data?.errors || [],
|
||||
data?.toolCalls || [],
|
||||
),
|
||||
[data?.messages, data?.llmCalls, data?.errors, data?.toolCalls],
|
||||
);
|
||||
|
||||
// State for expanded errors
|
||||
const [expandedErrorId, setExpandedErrorId] = useState<string | null>(null);
|
||||
const [expandedTurnId, setExpandedTurnId] = useState<string | null>(null);
|
||||
|
||||
// State for controlled tabs
|
||||
const [activeTab, setActiveTab] = useState<string>('executions');
|
||||
|
||||
// Ids the message list actually holds: a call may name a record that the
|
||||
// current window does not contain, and a dead link is worse than no link.
|
||||
const messageIds = useMemo(
|
||||
() => new Set(data?.messages.map((message) => message.id) ?? []),
|
||||
[data?.messages],
|
||||
);
|
||||
|
||||
// Function to jump to a message record
|
||||
const jumpToMessage = (messageId: string) => {
|
||||
setActiveTab('messages');
|
||||
setTimeout(() => {
|
||||
const turn = conversationTurns.find((item) =>
|
||||
item.messages.some((message) => message.id === messageId),
|
||||
);
|
||||
setExpandedTurnId(turn?.id ?? messageId);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const toggleTurnExpand = (turnId: string) => {
|
||||
setExpandedTurnId((current) => (current === turnId ? null : turnId));
|
||||
// Resolve the owning record server-side, including records outside this page.
|
||||
const jumpToMessage = (identifier: string) => {
|
||||
setActiveTab('executions');
|
||||
setSelectedExecution({ source: 'auto', id: identifier });
|
||||
};
|
||||
|
||||
const toggleErrorExpand = (errorId: string) => {
|
||||
@@ -194,7 +166,7 @@ function MonitoringPageContent() {
|
||||
{/* Filters and Refresh Button - Sticky */}
|
||||
<div className="sticky top-0 z-10 -mt-1 pb-5 pt-1 bg-background">
|
||||
<div>
|
||||
<Card className="flex flex-col gap-3 p-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between sm:gap-4 sm:p-4">
|
||||
<Card className="flex flex-col gap-4 p-4 xl:flex-row xl:items-end">
|
||||
<MonitoringFilters
|
||||
selectedBots={filterState.selectedBots}
|
||||
selectedPipelines={filterState.selectedPipelines}
|
||||
@@ -202,20 +174,22 @@ function MonitoringPageContent() {
|
||||
onBotsChange={setSelectedBots}
|
||||
onPipelinesChange={setSelectedPipelines}
|
||||
onTimeRangeChange={setTimeRange}
|
||||
source={executionSource}
|
||||
onSourceChange={setExecutionSource}
|
||||
mode={executionMode}
|
||||
onModeChange={setExecutionMode}
|
||||
statusGroup={executionStatus}
|
||||
onStatusGroupChange={setExecutionStatus}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2 border-t pt-3 xl:border-l xl:border-t-0 xl:pl-4 xl:pt-0 [&_button]:h-9">
|
||||
<Button variant="outline" size="sm" onClick={resetFilters}>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
{t('monitoring.filters.reset')}
|
||||
</Button>
|
||||
{canExport && <ExportDropdown filterState={filterState} />}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
className="flex-1 shadow-sm sm:flex-shrink-0 sm:flex-none"
|
||||
className="shadow-sm"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
{t('monitoring.refreshData')}
|
||||
@@ -258,14 +232,6 @@ function MonitoringPageContent() {
|
||||
className="text-sm text-muted-foreground space-y-1"
|
||||
role="status"
|
||||
>
|
||||
{data.totalCount.messages > data.messages.length && (
|
||||
<p>
|
||||
{t('monitoring.partialMessages', {
|
||||
shown: data.messages.length,
|
||||
total: data.totalCount.messages,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{data.totalCount.llmCalls + data.totalCount.embeddingCalls >
|
||||
data.modelCalls.length && (
|
||||
<p>
|
||||
@@ -308,9 +274,7 @@ function MonitoringPageContent() {
|
||||
<TabsTrigger value="executions" className="px-3 py-2 sm:px-6">
|
||||
{t('monitoring.execution.tabs.executions')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="messages" className="px-3 py-2 sm:px-6">
|
||||
{t('monitoring.tabs.messages')}
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger value="modelCalls" className="px-3 py-2 sm:px-6">
|
||||
{t('monitoring.tabs.modelCalls')}
|
||||
</TabsTrigger>
|
||||
@@ -413,30 +377,6 @@ function MonitoringPageContent() {
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent
|
||||
value="messages"
|
||||
className="m-0 min-h-0 flex-1 overflow-y-auto p-3 sm:p-6"
|
||||
>
|
||||
<div>
|
||||
{loading && <TabState loading rows={6} />}
|
||||
|
||||
{!loading && data && conversationTurns.length > 0 && (
|
||||
<ConversationTurnList
|
||||
turns={conversationTurns}
|
||||
expandedTurnId={expandedTurnId}
|
||||
onToggleTurn={toggleTurnExpand}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading && (!data || conversationTurns.length === 0) && (
|
||||
<TabState
|
||||
icon={<MessageSquare className="h-12 w-12" />}
|
||||
title={t('monitoring.messageList.noMessages')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent
|
||||
value="modelCalls"
|
||||
className="m-0 min-h-0 flex-1 overflow-y-auto p-3 sm:p-6"
|
||||
@@ -453,19 +393,9 @@ function MonitoringPageContent() {
|
||||
// A recorded call names the record it belongs to, so
|
||||
// the whole card opens it; without a match the card
|
||||
// stays static instead of pretending to be a link.
|
||||
const target = resolveCallTarget(
|
||||
call,
|
||||
executionResult?.items ?? [],
|
||||
messageIds,
|
||||
);
|
||||
const target = call.messageId;
|
||||
const openTarget = () => {
|
||||
if (!target) return;
|
||||
if (target.kind === 'execution') {
|
||||
setActiveTab('executions');
|
||||
setSelectedExecution(target.row);
|
||||
return;
|
||||
}
|
||||
jumpToMessage(target.id);
|
||||
if (target) jumpToMessage(target);
|
||||
};
|
||||
return (
|
||||
<Card
|
||||
@@ -504,31 +434,30 @@ function MonitoringPageContent() {
|
||||
{/* The stored id is the owning record: a
|
||||
Pipeline's message, or an Agent run's id
|
||||
that the card itself opens. */}
|
||||
{call.messageId &&
|
||||
messageIds.has(call.messageId) && (
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
Query ID: {call.messageId}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 px-1.5 text-xs"
|
||||
onClick={(event) => {
|
||||
// The card itself opens the call's
|
||||
// record; this button is the more
|
||||
// specific message target.
|
||||
event.stopPropagation();
|
||||
jumpToMessage(call.messageId!);
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="w-3 h-3 mr-1" />
|
||||
{t(
|
||||
'monitoring.messageList.viewConversation',
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{call.messageId && (
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
Query ID: {call.messageId}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 px-1.5 text-xs"
|
||||
onClick={(event) => {
|
||||
// The card itself opens the call's
|
||||
// record; this button is the more
|
||||
// specific message target.
|
||||
event.stopPropagation();
|
||||
jumpToMessage(call.messageId!);
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="w-3 h-3 mr-1" />
|
||||
{t(
|
||||
'monitoring.messageList.viewConversation',
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{/* Model Type Badge */}
|
||||
<Badge
|
||||
@@ -715,6 +644,8 @@ function MonitoringPageContent() {
|
||||
className="m-0 min-h-0 flex-1 overflow-y-auto p-3 sm:p-6"
|
||||
>
|
||||
<TokenMonitoring
|
||||
mode={executionMode}
|
||||
statusGroup={executionStatus}
|
||||
botIds={
|
||||
filterState.selectedBots.length > 0
|
||||
? filterState.selectedBots
|
||||
@@ -934,6 +865,7 @@ function MonitoringPageContent() {
|
||||
{/* Execution trace drawer */}
|
||||
<ExecutionDetailSheet
|
||||
row={selectedExecution}
|
||||
onSelect={setSelectedExecution}
|
||||
onClose={() => setSelectedExecution(null)}
|
||||
/>
|
||||
</div>
|
||||
@@ -941,9 +873,10 @@ function MonitoringPageContent() {
|
||||
}
|
||||
|
||||
export default function MonitoringPage() {
|
||||
const workspace = useCurrentWorkspace()?.workspace.uuid;
|
||||
return (
|
||||
<Suspense fallback={<LoadingPage />}>
|
||||
<MonitoringPageContent />
|
||||
<MonitoringPageContent key={workspace ?? 'pending'} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -164,6 +164,8 @@ export interface OverviewMetrics {
|
||||
}
|
||||
|
||||
export interface FilterState {
|
||||
mode?: 'all' | 'real' | 'debug';
|
||||
statusGroup?: string;
|
||||
selectedBots: string[];
|
||||
selectedPipelines: string[];
|
||||
timeRange: TimeRangeOption;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { ProcessorRun, ProcessorRunEvent } from './index';
|
||||
|
||||
export type ExecutionSource = 'agent' | 'pipeline';
|
||||
export type ExecutionSource = 'agent' | 'pipeline' | 'event';
|
||||
export type ExecutionSourceFilter = 'all' | ExecutionSource;
|
||||
export type ExecutionModeFilter = 'all' | 'real' | 'debug';
|
||||
|
||||
@@ -24,6 +22,9 @@ export interface ExecutionRow {
|
||||
| 'ignored'
|
||||
| string;
|
||||
title: string;
|
||||
input_preview?: string;
|
||||
event_type?: string;
|
||||
status_reason?: string | null;
|
||||
target_kind: 'agent' | 'pipeline' | 'processor' | string;
|
||||
target_id: string | null;
|
||||
target_name: string | null;
|
||||
@@ -117,20 +118,48 @@ export interface ExecutionCallRow {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AgentExecutionDetail {
|
||||
source: 'agent';
|
||||
run: ProcessorRun;
|
||||
events: ProcessorRunEvent[];
|
||||
export type ExecutionSection =
|
||||
| 'inputs'
|
||||
| 'outputs'
|
||||
| 'deliveries'
|
||||
| 'conversation'
|
||||
| 'related'
|
||||
| 'events'
|
||||
| 'llm_calls'
|
||||
| 'tool_calls'
|
||||
| 'errors';
|
||||
|
||||
export interface ExecutionDetailItem {
|
||||
id: string | number;
|
||||
timestamp_ms?: number | null;
|
||||
role?: string;
|
||||
origin?: string;
|
||||
event_type?: string;
|
||||
actor_name?: string;
|
||||
actor_id?: string;
|
||||
content?: unknown;
|
||||
attachments?: unknown[];
|
||||
status?: string;
|
||||
type?: string;
|
||||
sequence?: number;
|
||||
data?: Record<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
delivery?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ExecutionDetailPage {
|
||||
items: ExecutionDetailItem[];
|
||||
has_more: boolean;
|
||||
next_cursor: number | null;
|
||||
next_offset: number;
|
||||
}
|
||||
|
||||
export interface PipelineExecutionDetail {
|
||||
source: 'pipeline';
|
||||
message: PipelineMessageRow;
|
||||
llm_calls: ExecutionCallRow[];
|
||||
tool_calls: ExecutionCallRow[];
|
||||
errors: ExecutionCallRow[];
|
||||
export interface ExecutionDetail {
|
||||
source: ExecutionSource;
|
||||
row: ExecutionRow;
|
||||
pages: Partial<Record<ExecutionSection, ExecutionDetailPage>>;
|
||||
legacy_context: boolean;
|
||||
}
|
||||
|
||||
export type ExecutionDetail = AgentExecutionDetail | PipelineExecutionDetail;
|
||||
export type AgentExecutionDetail = ExecutionDetail & { source: 'agent' };
|
||||
export type PipelineExecutionDetail = ExecutionDetail & { source: 'pipeline' };
|
||||
|
||||
@@ -1980,6 +1980,8 @@ export class BackendClient extends BaseHttpClient {
|
||||
|
||||
// ============ Monitoring API ============
|
||||
public getMonitoringData(params: {
|
||||
mode?: ExecutionModeFilter;
|
||||
status?: string[];
|
||||
botId?: string[];
|
||||
pipelineId?: string[];
|
||||
startTime?: string;
|
||||
@@ -2103,6 +2105,8 @@ export class BackendClient extends BaseHttpClient {
|
||||
};
|
||||
}> {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params.mode) queryParams.append('mode', params.mode);
|
||||
params.status?.forEach((status) => queryParams.append('status', status));
|
||||
if (params.botId) {
|
||||
params.botId.forEach((id) => queryParams.append('botId', id));
|
||||
}
|
||||
@@ -2170,11 +2174,15 @@ export class BackendClient extends BaseHttpClient {
|
||||
}
|
||||
|
||||
public getExecutionDetail(
|
||||
source: ExecutionSource,
|
||||
source: ExecutionSource | 'auto',
|
||||
executionId: string,
|
||||
options: { section?: string; offset?: number; limit?: number } = {},
|
||||
): Promise<ExecutionDetail> {
|
||||
const params = new URLSearchParams(
|
||||
Object.entries(options).map(([key, value]) => [key, String(value)]),
|
||||
);
|
||||
return this.get(
|
||||
`/api/v1/monitoring/executions/${encodeURIComponent(source)}/${encodeURIComponent(executionId)}`,
|
||||
`/api/v1/monitoring/executions/${encodeURIComponent(source)}/${encodeURIComponent(executionId)}?${params}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2207,6 +2215,8 @@ export class BackendClient extends BaseHttpClient {
|
||||
}
|
||||
|
||||
public getTokenStatistics(params: {
|
||||
mode?: ExecutionModeFilter;
|
||||
status?: string[];
|
||||
botId?: string[];
|
||||
pipelineId?: string[];
|
||||
startTime?: string;
|
||||
@@ -2247,6 +2257,8 @@ export class BackendClient extends BaseHttpClient {
|
||||
bucket: string;
|
||||
}> {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params.mode) queryParams.append('mode', params.mode);
|
||||
params.status?.forEach((status) => queryParams.append('status', status));
|
||||
if (params.botId) {
|
||||
params.botId.forEach((id) => queryParams.append('botId', id));
|
||||
}
|
||||
|
||||
@@ -2456,6 +2456,30 @@ const enUS = {
|
||||
},
|
||||
},
|
||||
monitoring: {
|
||||
unified: {
|
||||
inputs: 'Event / input',
|
||||
outputs: 'Generated results',
|
||||
deliveries: 'Delivery records',
|
||||
conversation: 'Conversation context',
|
||||
related: 'Related executions',
|
||||
events: 'Execution events',
|
||||
llm_calls: 'Model calls',
|
||||
tool_calls: 'Tool calls',
|
||||
errors: 'Errors',
|
||||
payload: 'Details',
|
||||
metadata: 'Technical details',
|
||||
loadMore: 'Load more',
|
||||
noRecords: 'No records',
|
||||
contextHint:
|
||||
'Conversation context is not necessarily part of this execution.',
|
||||
unhandled: 'No processor run',
|
||||
roles: {
|
||||
user: 'User',
|
||||
assistant: 'Assistant',
|
||||
system: 'System',
|
||||
tool: 'Tool',
|
||||
},
|
||||
},
|
||||
execution: {
|
||||
title: 'Executions',
|
||||
subtitle:
|
||||
@@ -2499,24 +2523,28 @@ const enUS = {
|
||||
loading: 'Loading executions...',
|
||||
triggeredBy: 'Triggered by {{trigger}}',
|
||||
cards: {
|
||||
latencyHint:
|
||||
'P50/P95: about 50%/95% of runs finish within this time. Based on recent completed Agent runs, excluding queue time.',
|
||||
tokensHint:
|
||||
'Only recorded usage is included. Totals may be incomplete when models do not return usage.',
|
||||
totalExecutions: 'Executions',
|
||||
bySource: '{{agent}} agent · {{pipeline}} pipeline',
|
||||
successRate: 'Execution Success Rate',
|
||||
successRateHint:
|
||||
'Completed ÷ (completed + failed). Ignored events and in-flight runs are excluded from the denominator.',
|
||||
denominator: '{{completed}} of {{denominator}} settled',
|
||||
'Successes ÷ (successes + failures). In-progress, ignored and cancelled runs are excluded.',
|
||||
denominator: '{{completed}} succeeded out of {{denominator}} finished',
|
||||
running: 'Running',
|
||||
queued: 'Queued',
|
||||
waiting: 'Waiting input',
|
||||
failed: 'Failed',
|
||||
ignored: 'Ignored',
|
||||
latency: 'Duration P50 / P95',
|
||||
latency: 'Execution time',
|
||||
p50: 'P50',
|
||||
p95: 'P95',
|
||||
sample: 'sample of {{count}} runs',
|
||||
sample: 'Based on {{count}} runs',
|
||||
tokens: 'Tokens',
|
||||
coverage: 'usage on {{withUsage}}/{{calls}} calls',
|
||||
hiddenDebug: '{{count}} debug runs hidden',
|
||||
debugRuns: 'Includes {{count}} debug runs',
|
||||
},
|
||||
detail: {
|
||||
title: 'Execution',
|
||||
@@ -2553,6 +2581,9 @@ const enUS = {
|
||||
pipeline: 'Pipeline',
|
||||
allBots: 'All Bots',
|
||||
selectBot: 'Select Bot',
|
||||
processor: 'Processor',
|
||||
allProcessors: 'All Processors',
|
||||
selectProcessor: 'Select Processor',
|
||||
allPipelines: 'All Pipelines',
|
||||
selectPipeline: 'Select Pipeline',
|
||||
loading: 'Loading...',
|
||||
|
||||
@@ -2508,6 +2508,30 @@ const esES = {
|
||||
},
|
||||
},
|
||||
monitoring: {
|
||||
unified: {
|
||||
inputs: 'Evento y entrada',
|
||||
outputs: 'Resultados generados',
|
||||
deliveries: 'Registros de envío',
|
||||
conversation: 'Contexto de conversación',
|
||||
related: 'Ejecuciones relacionadas',
|
||||
events: 'Eventos de ejecución',
|
||||
llm_calls: 'Llamadas al modelo',
|
||||
tool_calls: 'Llamadas a herramientas',
|
||||
errors: 'Errores',
|
||||
payload: 'Detalles',
|
||||
metadata: 'Información técnica',
|
||||
loadMore: 'Cargar más',
|
||||
noRecords: 'Sin registros',
|
||||
contextHint:
|
||||
'Otros mensajes no pertenecen necesariamente a esta ejecución.',
|
||||
unhandled: 'Sin ejecución',
|
||||
roles: {
|
||||
user: 'Usuario',
|
||||
assistant: 'Asistente',
|
||||
system: 'Sistema',
|
||||
tool: 'Herramienta',
|
||||
},
|
||||
},
|
||||
execution: {
|
||||
title: 'Ejecuciones',
|
||||
subtitle:
|
||||
@@ -2551,24 +2575,28 @@ const esES = {
|
||||
loading: 'Cargando ejecuciones...',
|
||||
triggeredBy: 'Activada por {{trigger}}',
|
||||
cards: {
|
||||
latencyHint:
|
||||
'P50/P95: cerca del 50%/95% termina dentro de este tiempo. Solo ejecuciones recientes de Agent completadas, sin espera en cola.',
|
||||
tokensHint:
|
||||
'Solo se suma el uso registrado. El total puede estar incompleto si el modelo no devuelve el uso.',
|
||||
totalExecutions: 'Ejecuciones',
|
||||
bySource: '{{agent}} de agentes · {{pipeline}} de pipelines',
|
||||
successRate: 'Tasa de éxito de ejecución',
|
||||
successRateHint:
|
||||
'Completadas ÷ (completadas + fallidas). Se excluyen del denominador los eventos ignorados y las ejecuciones en curso.',
|
||||
'Éxitos ÷ (éxitos + fallos). Se excluyen ejecuciones en curso, ignoradas y canceladas.',
|
||||
denominator: '{{completed}} de {{denominator}} finalizadas',
|
||||
running: 'En curso',
|
||||
queued: 'En cola',
|
||||
waiting: 'Esperando entrada',
|
||||
failed: 'Fallida',
|
||||
ignored: 'Ignorada',
|
||||
latency: 'Duración P50 / P95',
|
||||
latency: 'Tiempo de ejecución',
|
||||
p50: 'P50',
|
||||
p95: 'P95',
|
||||
sample: 'muestra de {{count}} ejecuciones',
|
||||
tokens: 'Tokens',
|
||||
coverage: 'uso registrado en {{withUsage}}/{{calls}} llamadas',
|
||||
hiddenDebug: '{{count}} ejecuciones de depuración ocultas',
|
||||
debugRuns: 'Incluye {{count}} ejecuciones de depuración',
|
||||
},
|
||||
detail: {
|
||||
title: 'Ejecución',
|
||||
@@ -2606,6 +2634,9 @@ const esES = {
|
||||
pipeline: 'Pipeline',
|
||||
allBots: 'Todos los Bots',
|
||||
selectBot: 'Seleccionar Bot',
|
||||
processor: 'Procesador',
|
||||
allProcessors: 'Todos los procesadores',
|
||||
selectProcessor: 'Seleccionar procesador',
|
||||
allPipelines: 'Todos los Pipelines',
|
||||
selectPipeline: 'Seleccionar Pipeline',
|
||||
loading: 'Cargando...',
|
||||
|
||||
@@ -2472,6 +2472,29 @@ const jaJP = {
|
||||
},
|
||||
},
|
||||
monitoring: {
|
||||
unified: {
|
||||
inputs: 'イベントと入力',
|
||||
outputs: '生成結果',
|
||||
deliveries: '送信記録',
|
||||
conversation: '会話の前後関係',
|
||||
related: '関連する実行',
|
||||
events: '実行イベント',
|
||||
llm_calls: 'モデル呼び出し',
|
||||
tool_calls: 'ツール呼び出し',
|
||||
errors: 'エラー',
|
||||
payload: '詳細',
|
||||
metadata: '技術情報',
|
||||
loadMore: 'さらに読み込む',
|
||||
noRecords: '記録なし',
|
||||
contextHint: '会話内の他のメッセージがこの実行に属するとは限りません。',
|
||||
unhandled: '処理なし',
|
||||
roles: {
|
||||
user: 'ユーザー',
|
||||
assistant: 'アシスタント',
|
||||
system: 'システム',
|
||||
tool: 'ツール',
|
||||
},
|
||||
},
|
||||
execution: {
|
||||
title: '実行履歴',
|
||||
subtitle:
|
||||
@@ -2515,24 +2538,28 @@ const jaJP = {
|
||||
loading: '実行履歴を読み込み中...',
|
||||
triggeredBy: '{{trigger}} により開始',
|
||||
cards: {
|
||||
latencyHint:
|
||||
'P50/P95:実行の約 50%/95% がこの時間内に完了します。最近の完了済み Agent 実行が対象で、キュー待ちは除きます。',
|
||||
tokensHint:
|
||||
'記録済みの使用量のみ集計します。モデルが使用量を返さない場合、合計は不完全になります。',
|
||||
totalExecutions: '実行履歴',
|
||||
bySource: 'エージェント {{agent}} 件 · パイプライン {{pipeline}} 件',
|
||||
successRate: '実行成功率',
|
||||
successRateHint:
|
||||
'完了 ÷(完了+失敗)。無視されたイベントと実行中の処理は分母に含まれません。',
|
||||
'成功数 ÷(成功数 + 失敗数)。実行中・無視・キャンセルは除きます。',
|
||||
denominator: '終了済み {{denominator}} 件中 {{completed}} 件が完了',
|
||||
running: '実行中',
|
||||
queued: '待機中',
|
||||
waiting: '入力待ち',
|
||||
failed: '失敗',
|
||||
ignored: '無視',
|
||||
latency: '所要時間 P50 / P95',
|
||||
latency: '実行時間',
|
||||
p50: 'P50',
|
||||
p95: 'P95',
|
||||
sample: '{{count}} 件の実行を集計',
|
||||
tokens: 'トークン',
|
||||
coverage: '{{calls}} 回中 {{withUsage}} 回で使用量を取得',
|
||||
hiddenDebug: 'デバッグ実行 {{count}} 件を非表示',
|
||||
debugRuns: 'うちデバッグ実行 {{count}} 件',
|
||||
},
|
||||
detail: {
|
||||
title: '実行',
|
||||
@@ -2570,6 +2597,9 @@ const jaJP = {
|
||||
pipeline: 'パイプライン',
|
||||
allBots: 'すべてのボット',
|
||||
selectBot: 'ボットを選択',
|
||||
processor: 'プロセッサー',
|
||||
allProcessors: 'すべてのプロセッサー',
|
||||
selectProcessor: 'プロセッサーを選択',
|
||||
allPipelines: 'すべてのパイプライン',
|
||||
selectPipeline: 'パイプラインを選択',
|
||||
loading: '読み込み中...',
|
||||
|
||||
@@ -2480,6 +2480,29 @@ const ruRU = {
|
||||
},
|
||||
},
|
||||
monitoring: {
|
||||
unified: {
|
||||
inputs: 'Событие и ввод',
|
||||
outputs: 'Сгенерированные результаты',
|
||||
deliveries: 'Записи отправки',
|
||||
conversation: 'Контекст диалога',
|
||||
related: 'Связанные запуски',
|
||||
events: 'События выполнения',
|
||||
llm_calls: 'Вызовы модели',
|
||||
tool_calls: 'Вызовы инструментов',
|
||||
errors: 'Ошибки',
|
||||
payload: 'Подробности',
|
||||
metadata: 'Технические сведения',
|
||||
loadMore: 'Загрузить ещё',
|
||||
noRecords: 'Нет записей',
|
||||
contextHint: 'Другие сообщения могут не относиться к этому запуску.',
|
||||
unhandled: 'Обработчик не запущен',
|
||||
roles: {
|
||||
user: 'Пользователь',
|
||||
assistant: 'Ассистент',
|
||||
system: 'Система',
|
||||
tool: 'Инструмент',
|
||||
},
|
||||
},
|
||||
execution: {
|
||||
title: 'Выполнения',
|
||||
subtitle:
|
||||
@@ -2523,24 +2546,28 @@ const ruRU = {
|
||||
loading: 'Загрузка выполнений...',
|
||||
triggeredBy: 'Запущено: {{trigger}}',
|
||||
cards: {
|
||||
latencyHint:
|
||||
'P50/P95: около 50%/95% запусков завершаются за это время. Только недавние завершённые запуски Agent, без ожидания в очереди.',
|
||||
tokensHint:
|
||||
'Учитывается только записанный расход. Итог может быть неполным, если модель не возвращает расход.',
|
||||
totalExecutions: 'Выполнения',
|
||||
bySource: 'Агенты: {{agent}} · Конвейеры: {{pipeline}}',
|
||||
successRate: 'Доля успешных выполнений',
|
||||
successRateHint:
|
||||
'Завершённые ÷ (завершённые + неудачные). Пропущенные события и незавершённые выполнения не входят в знаменатель.',
|
||||
'Успешные ÷ (успешные + неудачные). Текущие, пропущенные и отменённые запуски исключены.',
|
||||
denominator: 'Успешно: {{completed}} из {{denominator}} завершившихся',
|
||||
running: 'Выполняется',
|
||||
queued: 'В очереди',
|
||||
waiting: 'Ожидание ввода',
|
||||
failed: 'Ошибка',
|
||||
ignored: 'Пропущено',
|
||||
latency: 'Длительность P50 / P95',
|
||||
latency: 'Время выполнения',
|
||||
p50: 'P50',
|
||||
p95: 'P95',
|
||||
sample: 'выборка: {{count}} выполнений',
|
||||
tokens: 'Токены',
|
||||
coverage: 'расход указан для {{withUsage}}/{{calls}} вызовов',
|
||||
hiddenDebug: 'Скрыто отладочных выполнений: {{count}}',
|
||||
debugRuns: 'В том числе отладочных выполнений: {{count}}',
|
||||
},
|
||||
detail: {
|
||||
title: 'Выполнение',
|
||||
@@ -2578,6 +2605,9 @@ const ruRU = {
|
||||
pipeline: 'Конвейер',
|
||||
allBots: 'Все боты',
|
||||
selectBot: 'Выберите бота',
|
||||
processor: 'Обработчик',
|
||||
allProcessors: 'Все обработчики',
|
||||
selectProcessor: 'Выберите обработчик',
|
||||
allPipelines: 'Все конвейеры',
|
||||
selectPipeline: 'Выберите конвейер',
|
||||
loading: 'Загрузка...',
|
||||
|
||||
@@ -2407,6 +2407,29 @@ const thTH = {
|
||||
},
|
||||
},
|
||||
monitoring: {
|
||||
unified: {
|
||||
inputs: 'เหตุการณ์และข้อมูลเข้า',
|
||||
outputs: 'ผลลัพธ์ที่สร้าง',
|
||||
deliveries: 'บันทึกการส่ง',
|
||||
conversation: 'บริบทการสนทนา',
|
||||
related: 'การทำงานที่เกี่ยวข้อง',
|
||||
events: 'เหตุการณ์การทำงาน',
|
||||
llm_calls: 'การเรียกโมเดล',
|
||||
tool_calls: 'การเรียกเครื่องมือ',
|
||||
errors: 'ข้อผิดพลาด',
|
||||
payload: 'รายละเอียด',
|
||||
metadata: 'ข้อมูลทางเทคนิค',
|
||||
loadMore: 'โหลดเพิ่มเติม',
|
||||
noRecords: 'ไม่มีบันทึก',
|
||||
contextHint: 'ข้อความอื่นอาจไม่ใช่ส่วนหนึ่งของการทำงานนี้',
|
||||
unhandled: 'ไม่ได้เริ่มตัวประมวลผล',
|
||||
roles: {
|
||||
user: 'ผู้ใช้',
|
||||
assistant: 'ผู้ช่วย',
|
||||
system: 'ระบบ',
|
||||
tool: 'เครื่องมือ',
|
||||
},
|
||||
},
|
||||
execution: {
|
||||
title: 'การทำงาน',
|
||||
subtitle:
|
||||
@@ -2450,11 +2473,15 @@ const thTH = {
|
||||
loading: 'กำลังโหลดการทำงาน...',
|
||||
triggeredBy: 'เรียกใช้โดย {{trigger}}',
|
||||
cards: {
|
||||
latencyHint:
|
||||
'P50/P95: ประมาณ 50%/95% ทำงานเสร็จภายในเวลานี้ ใช้เฉพาะ Agent ที่เสร็จล่าสุด ไม่รวมเวลารอคิว',
|
||||
tokensHint:
|
||||
'รวมเฉพาะการใช้ที่บันทึกไว้ ผลรวมอาจไม่ครบหากโมเดลไม่ส่งข้อมูลการใช้กลับมา',
|
||||
totalExecutions: 'การทำงาน',
|
||||
bySource: 'เอเจนต์ {{agent}} · ไปป์ไลน์ {{pipeline}}',
|
||||
successRate: 'อัตราการทำงานสำเร็จ',
|
||||
successRateHint:
|
||||
'เสร็จสิ้น ÷ (เสร็จสิ้น + ล้มเหลว) ไม่นับเหตุการณ์ที่ละเว้นและการทำงานที่ยังไม่สิ้นสุดในตัวหาร',
|
||||
'สำเร็จ ÷ (สำเร็จ + ล้มเหลว) ไม่รวมงานที่กำลังทำงาน ถูกละเว้น หรือยกเลิก',
|
||||
denominator:
|
||||
'สำเร็จ {{completed}} จาก {{denominator}} รายการที่สิ้นสุดแล้ว',
|
||||
running: 'กำลังทำงาน',
|
||||
@@ -2462,13 +2489,13 @@ const thTH = {
|
||||
waiting: 'รอการป้อนข้อมูล',
|
||||
failed: 'ล้มเหลว',
|
||||
ignored: 'ละเว้นแล้ว',
|
||||
latency: 'ระยะเวลา P50 / P95',
|
||||
latency: 'เวลาทำงาน',
|
||||
p50: 'P50',
|
||||
p95: 'P95',
|
||||
sample: 'ตัวอย่างการทำงาน {{count}} ครั้ง',
|
||||
tokens: 'โทเคน',
|
||||
coverage: 'มีข้อมูลการใช้งาน {{withUsage}}/{{calls}} ครั้ง',
|
||||
hiddenDebug: 'ซ่อนการทำงานดีบัก {{count}} ครั้ง',
|
||||
debugRuns: 'รวมการทำงานดีบัก {{count}} ครั้ง',
|
||||
},
|
||||
detail: {
|
||||
title: 'การทำงาน',
|
||||
@@ -2505,6 +2532,9 @@ const thTH = {
|
||||
pipeline: 'Pipeline',
|
||||
allBots: 'Bot ทั้งหมด',
|
||||
selectBot: 'เลือก Bot',
|
||||
processor: 'ตัวประมวลผล',
|
||||
allProcessors: 'ตัวประมวลผลทั้งหมด',
|
||||
selectProcessor: 'เลือกตัวประมวลผล',
|
||||
allPipelines: 'Pipeline ทั้งหมด',
|
||||
selectPipeline: 'เลือก Pipeline',
|
||||
loading: 'กำลังโหลด...',
|
||||
|
||||
@@ -2442,6 +2442,29 @@ const viVN = {
|
||||
},
|
||||
},
|
||||
monitoring: {
|
||||
unified: {
|
||||
inputs: 'Sự kiện và đầu vào',
|
||||
outputs: 'Kết quả tạo ra',
|
||||
deliveries: 'Bản ghi gửi',
|
||||
conversation: 'Ngữ cảnh hội thoại',
|
||||
related: 'Lượt chạy liên quan',
|
||||
events: 'Sự kiện thực thi',
|
||||
llm_calls: 'Lần gọi mô hình',
|
||||
tool_calls: 'Lần gọi công cụ',
|
||||
errors: 'Lỗi',
|
||||
payload: 'Chi tiết',
|
||||
metadata: 'Thông tin kỹ thuật',
|
||||
loadMore: 'Tải thêm',
|
||||
noRecords: 'Chưa có bản ghi',
|
||||
contextHint: 'Các tin nhắn khác không nhất thiết thuộc lượt chạy này.',
|
||||
unhandled: 'Chưa chạy bộ xử lý',
|
||||
roles: {
|
||||
user: 'Người dùng',
|
||||
assistant: 'Trợ lý',
|
||||
system: 'Hệ thống',
|
||||
tool: 'Công cụ',
|
||||
},
|
||||
},
|
||||
execution: {
|
||||
title: 'Lượt thực thi',
|
||||
subtitle:
|
||||
@@ -2485,11 +2508,15 @@ const viVN = {
|
||||
loading: 'Đang tải lượt thực thi...',
|
||||
triggeredBy: 'Được kích hoạt bởi {{trigger}}',
|
||||
cards: {
|
||||
latencyHint:
|
||||
'P50/P95: khoảng 50%/95% lượt chạy hoàn tất trong thời gian này. Chỉ tính Agent đã hoàn tất gần đây, không gồm thời gian xếp hàng.',
|
||||
tokensHint:
|
||||
'Chỉ cộng mức dùng đã ghi. Tổng có thể thiếu nếu mô hình không trả về dữ liệu sử dụng.',
|
||||
totalExecutions: 'Lượt thực thi',
|
||||
bySource: 'Tác nhân {{agent}} · Pipeline {{pipeline}}',
|
||||
successRate: 'Tỷ lệ thực thi thành công',
|
||||
successRateHint:
|
||||
'Hoàn thành ÷ (hoàn thành + thất bại). Không tính sự kiện bị bỏ qua và lượt chưa kết thúc vào mẫu số.',
|
||||
'Thành công ÷ (thành công + thất bại). Không tính lượt đang chạy, bỏ qua hoặc đã hủy.',
|
||||
denominator:
|
||||
'Thành công {{completed}} trong {{denominator}} lượt đã kết thúc',
|
||||
running: 'Đang chạy',
|
||||
@@ -2497,13 +2524,13 @@ const viVN = {
|
||||
waiting: 'Đang chờ nhập liệu',
|
||||
failed: 'Thất bại',
|
||||
ignored: 'Đã bỏ qua',
|
||||
latency: 'Thời lượng P50 / P95',
|
||||
latency: 'Thời gian thực thi',
|
||||
p50: 'P50',
|
||||
p95: 'P95',
|
||||
sample: 'Mẫu gồm {{count}} lượt thực thi',
|
||||
tokens: 'Token',
|
||||
coverage: 'Có dữ liệu sử dụng cho {{withUsage}}/{{calls}} lệnh gọi',
|
||||
hiddenDebug: 'Đã ẩn {{count}} lượt gỡ lỗi',
|
||||
debugRuns: 'Bao gồm {{count}} lượt gỡ lỗi',
|
||||
},
|
||||
detail: {
|
||||
title: 'Lượt thực thi',
|
||||
@@ -2540,6 +2567,9 @@ const viVN = {
|
||||
pipeline: 'Pipeline',
|
||||
allBots: 'Tất cả Bot',
|
||||
selectBot: 'Chọn Bot',
|
||||
processor: 'Bộ xử lý',
|
||||
allProcessors: 'Tất cả bộ xử lý',
|
||||
selectProcessor: 'Chọn bộ xử lý',
|
||||
allPipelines: 'Tất cả Pipeline',
|
||||
selectPipeline: 'Chọn Pipeline',
|
||||
loading: 'Đang tải...',
|
||||
|
||||
@@ -2308,6 +2308,29 @@ const zhHans = {
|
||||
},
|
||||
},
|
||||
monitoring: {
|
||||
unified: {
|
||||
inputs: '事件与输入',
|
||||
outputs: '生成结果',
|
||||
deliveries: '发送记录',
|
||||
conversation: '会话上下文',
|
||||
related: '关联执行',
|
||||
events: '执行事件',
|
||||
llm_calls: '模型调用',
|
||||
tool_calls: '工具调用',
|
||||
errors: '错误',
|
||||
payload: '查看详情',
|
||||
metadata: '技术信息',
|
||||
loadMore: '加载更多',
|
||||
noRecords: '暂无记录',
|
||||
contextHint: '会话中的其他消息不一定属于本次执行。',
|
||||
unhandled: '未启动处理器',
|
||||
roles: {
|
||||
user: '用户',
|
||||
assistant: '助手',
|
||||
system: '系统',
|
||||
tool: '工具',
|
||||
},
|
||||
},
|
||||
execution: {
|
||||
title: '执行记录',
|
||||
subtitle: '每条入站事件及其触发的执行,覆盖 Agent 与 Pipeline。',
|
||||
@@ -2350,24 +2373,28 @@ const zhHans = {
|
||||
loading: '加载执行记录中...',
|
||||
triggeredBy: '触发:{{trigger}}',
|
||||
cards: {
|
||||
latencyHint:
|
||||
'P50/P95:约 50%/95% 的执行能在此时间内完成。仅统计最近已完成的 Agent 执行,不含排队时间。',
|
||||
tokensHint:
|
||||
'仅汇总已记录的用量;部分模型未返回用量时,总数可能不完整。',
|
||||
totalExecutions: '执行总数',
|
||||
bySource: 'Agent {{agent}} · Pipeline {{pipeline}}',
|
||||
successRate: '执行成功率',
|
||||
successRateHint:
|
||||
'已完成 ÷(已完成 + 失败)。已忽略事件与执行中的任务不计入分母。',
|
||||
denominator: '{{completed}} / {{denominator}} 已结束',
|
||||
'成功次数 ÷(成功 + 失败)。不计进行中、已忽略和已取消的执行。',
|
||||
denominator: '{{denominator}} 次已结束,{{completed}} 次成功',
|
||||
running: '运行中',
|
||||
queued: '排队中',
|
||||
waiting: '等待交互',
|
||||
waiting: '等待输入',
|
||||
failed: '失败',
|
||||
ignored: '已忽略',
|
||||
latency: '耗时 P50 / P95',
|
||||
latency: '执行耗时',
|
||||
p50: 'P50',
|
||||
p95: 'P95',
|
||||
sample: '样本 {{count}} 次',
|
||||
sample: '根据 {{count}} 次执行计算',
|
||||
tokens: 'Token',
|
||||
coverage: '{{withUsage}}/{{calls}} 次调用有用量',
|
||||
hiddenDebug: '已隐藏 {{count}} 条调试运行',
|
||||
debugRuns: '其中 {{count}} 条为调试执行',
|
||||
},
|
||||
detail: {
|
||||
title: '执行详情',
|
||||
@@ -2404,6 +2431,9 @@ const zhHans = {
|
||||
pipeline: '流水线',
|
||||
allBots: '全部机器人',
|
||||
selectBot: '选择机器人',
|
||||
processor: '处理器',
|
||||
allProcessors: '全部处理器',
|
||||
selectProcessor: '选择处理器',
|
||||
allPipelines: '全部流水线',
|
||||
selectPipeline: '选择流水线',
|
||||
loading: '加载中...',
|
||||
|
||||
@@ -2309,6 +2309,29 @@ const zhHant = {
|
||||
},
|
||||
},
|
||||
monitoring: {
|
||||
unified: {
|
||||
inputs: '事件與輸入',
|
||||
outputs: '生成結果',
|
||||
deliveries: '傳送記錄',
|
||||
conversation: '對話上下文',
|
||||
related: '關聯執行',
|
||||
events: '執行事件',
|
||||
llm_calls: '模型呼叫',
|
||||
tool_calls: '工具呼叫',
|
||||
errors: '錯誤',
|
||||
payload: '查看詳情',
|
||||
metadata: '技術資訊',
|
||||
loadMore: '載入更多',
|
||||
noRecords: '暫無記錄',
|
||||
contextHint: '對話中的其他訊息不一定屬於本次執行。',
|
||||
unhandled: '未啟動處理器',
|
||||
roles: {
|
||||
user: '使用者',
|
||||
assistant: '助手',
|
||||
system: '系統',
|
||||
tool: '工具',
|
||||
},
|
||||
},
|
||||
execution: {
|
||||
title: '執行記錄',
|
||||
subtitle: '每條入站事件及其觸發的執行,涵蓋 Agent 與 Pipeline。',
|
||||
@@ -2351,24 +2374,28 @@ const zhHant = {
|
||||
triggeredBy: '觸發:{{trigger}}',
|
||||
loading: '載入執行記錄中...',
|
||||
cards: {
|
||||
latencyHint:
|
||||
'P50/P95:約 50%/95% 的執行能在此時間內完成。僅統計最近已完成的 Agent 執行,不含排隊時間。',
|
||||
tokensHint:
|
||||
'僅加總已記錄的用量;部分模型未回傳用量時,總數可能不完整。',
|
||||
totalExecutions: '執行總數',
|
||||
bySource: 'Agent {{agent}} · Pipeline {{pipeline}}',
|
||||
successRate: '執行成功率',
|
||||
successRateHint:
|
||||
'已完成 ÷(已完成 + 失敗)。已忽略事件與執行中的任務不計入分母。',
|
||||
'成功次數 ÷(成功 + 失敗)。不計進行中、已忽略及已取消的執行。',
|
||||
denominator: '{{completed}} / {{denominator}} 已結束',
|
||||
running: '執行中',
|
||||
queued: '排隊中',
|
||||
waiting: '等待互動',
|
||||
failed: '失敗',
|
||||
ignored: '已忽略',
|
||||
latency: '耗時 P50 / P95',
|
||||
latency: '執行耗時',
|
||||
p50: 'P50',
|
||||
p95: 'P95',
|
||||
sample: '樣本 {{count}} 次',
|
||||
tokens: 'Token',
|
||||
coverage: '{{withUsage}}/{{calls}} 次呼叫有用量',
|
||||
hiddenDebug: '已隱藏 {{count}} 筆偵錯執行',
|
||||
debugRuns: '其中 {{count}} 筆為偵錯執行',
|
||||
},
|
||||
detail: {
|
||||
title: '執行詳情',
|
||||
@@ -2405,6 +2432,9 @@ const zhHant = {
|
||||
pipeline: '流水線',
|
||||
allBots: '全部機器人',
|
||||
selectBot: '選擇機器人',
|
||||
processor: '處理器',
|
||||
allProcessors: '全部處理器',
|
||||
selectProcessor: '選擇處理器',
|
||||
allPipelines: '全部流水線',
|
||||
selectPipeline: '選擇流水線',
|
||||
loading: '載入中...',
|
||||
|
||||
Reference in New Issue
Block a user