mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-14 08:26:07 +00:00
Improve monitoring conversation turns
This commit is contained in:
@@ -0,0 +1,476 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
AlertCircle,
|
||||
Bot,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Clock,
|
||||
Cpu,
|
||||
Hash,
|
||||
User,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { MessageContentRenderer } from './MessageContentRenderer';
|
||||
import {
|
||||
ConversationTurn,
|
||||
hasRenderableMessageContent,
|
||||
} from '../utils/conversationTurns';
|
||||
import { MonitoringMessage } from '../types/monitoring';
|
||||
|
||||
interface ConversationTurnListProps {
|
||||
turns: ConversationTurn[];
|
||||
expandedTurnId: string | null;
|
||||
onToggleTurn: (turnId: string) => void;
|
||||
}
|
||||
|
||||
function shortId(id?: string) {
|
||||
if (!id) return '-';
|
||||
if (id.length <= 12) return id;
|
||||
return `${id.slice(0, 8)}...${id.slice(-4)}`;
|
||||
}
|
||||
|
||||
function formatDuration(ms: number) {
|
||||
if (!ms) return '0ms';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
return `${(ms / 1000).toFixed(2)}s`;
|
||||
}
|
||||
|
||||
function roleLabel(message: MonitoringMessage | undefined) {
|
||||
const role = message?.role?.toLowerCase();
|
||||
if (role === 'assistant') return 'assistant';
|
||||
if (role === 'user') return 'user';
|
||||
return 'message';
|
||||
}
|
||||
|
||||
function statusClass(level: ConversationTurn['level']) {
|
||||
if (level === 'error') {
|
||||
return 'border-red-200 bg-red-50 text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300';
|
||||
}
|
||||
if (level === 'warning') {
|
||||
return 'border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-900 dark:bg-yellow-950/40 dark:text-yellow-300';
|
||||
}
|
||||
return 'border-green-200 bg-green-50 text-green-700 dark:border-green-900 dark:bg-green-950/40 dark:text-green-300';
|
||||
}
|
||||
|
||||
function Metric({
|
||||
icon,
|
||||
label,
|
||||
tone = 'default',
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
tone?: 'default' | 'error';
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex h-7 items-center gap-1.5 rounded-md border px-2 text-xs font-medium',
|
||||
tone === 'error'
|
||||
? 'border-red-200 bg-red-50 text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300'
|
||||
: 'border-border bg-background text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function MetaItem({ label, value }: { label: string; value?: string }) {
|
||||
return (
|
||||
<div className="min-w-0 rounded-md bg-background px-3 py-2">
|
||||
<div className="text-xs text-muted-foreground">{label}</div>
|
||||
<div className="truncate text-sm font-medium text-foreground">
|
||||
{value || '-'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageLane({
|
||||
label,
|
||||
icon,
|
||||
content,
|
||||
empty,
|
||||
maxLines,
|
||||
}: {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
content?: string;
|
||||
empty: string;
|
||||
maxLines: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-[5.25rem_minmax(0,1fr)] items-start gap-3 text-sm sm:grid-cols-[6rem_minmax(0,1fr)]">
|
||||
<div className="flex h-7 items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
<div className="min-w-0 rounded-md bg-muted/45 px-3 py-2 text-foreground">
|
||||
{content && hasRenderableMessageContent(content) ? (
|
||||
<MessageContentRenderer content={content} maxLines={maxLines} />
|
||||
) : (
|
||||
<span className="italic text-muted-foreground">{empty}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandedMessage({
|
||||
message,
|
||||
label,
|
||||
}: {
|
||||
message: MonitoringMessage;
|
||||
label: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="border-t border-border/70 py-3 first:border-t-0 first:pt-0 last:pb-0">
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<span className="rounded-md bg-muted px-2 py-1 font-medium text-foreground">
|
||||
{label}
|
||||
</span>
|
||||
<span>{message.timestamp.toLocaleString()}</span>
|
||||
<span className="font-mono">ID: {shortId(message.id)}</span>
|
||||
</div>
|
||||
<div className="text-sm leading-6 text-foreground">
|
||||
<MessageContentRenderer content={message.messageContent} maxLines={4} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConversationTurnList({
|
||||
turns,
|
||||
expandedTurnId,
|
||||
onToggleTurn,
|
||||
}: ConversationTurnListProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<span className="font-medium text-foreground">
|
||||
{t('monitoring.messageList.turns', {
|
||||
defaultValue: '{{count}} 轮对话',
|
||||
count: turns.length,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{turns.map((turn) => {
|
||||
const expanded = expandedTurnId === turn.id;
|
||||
const firstAssistant = turn.assistantMessages[0];
|
||||
const assistantOverflow = Math.max(
|
||||
turn.assistantMessages.length - 1,
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={turn.id}
|
||||
className={cn(
|
||||
'overflow-hidden rounded-xl border bg-card transition-colors',
|
||||
turn.level === 'error' && 'border-red-200 dark:border-red-900',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="cursor-pointer p-3 outline-none transition-colors hover:bg-accent/60 focus-visible:ring-2 focus-visible:ring-ring sm:p-5"
|
||||
onClick={() => onToggleTurn(turn.id)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
onToggleTurn(turn.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-2 flex min-w-0 items-center gap-2">
|
||||
{expanded ? (
|
||||
<ChevronDown className="h-5 w-5 shrink-0 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-5 w-5 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<span className="truncate font-mono text-xs text-muted-foreground">
|
||||
Turn: {shortId(turn.id)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-3 flex min-w-0 flex-wrap items-center gap-2">
|
||||
<span className="truncate text-sm font-medium text-foreground">
|
||||
{turn.botName}
|
||||
</span>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span className="truncate text-sm text-muted-foreground">
|
||||
{turn.pipelineName}
|
||||
</span>
|
||||
{turn.runnerName && (
|
||||
<>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span className="truncate text-sm text-muted-foreground">
|
||||
{turn.runnerName}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<MessageLane
|
||||
label={t('monitoring.messageList.userMessage', {
|
||||
defaultValue: '用户',
|
||||
})}
|
||||
icon={<User className="h-3.5 w-3.5" />}
|
||||
content={turn.userMessage?.messageContent}
|
||||
empty={t('monitoring.messageList.noUserMessage', {
|
||||
defaultValue: '未记录用户输入',
|
||||
})}
|
||||
maxLines={2}
|
||||
/>
|
||||
<MessageLane
|
||||
label={
|
||||
assistantOverflow > 0
|
||||
? t('monitoring.messageList.assistantMessageCount', {
|
||||
defaultValue: '助手 +{{count}}',
|
||||
count: assistantOverflow,
|
||||
})
|
||||
: t('monitoring.messageList.assistantMessage', {
|
||||
defaultValue: '助手',
|
||||
})
|
||||
}
|
||||
icon={<Bot className="h-3.5 w-3.5" />}
|
||||
content={firstAssistant?.messageContent}
|
||||
empty={t('monitoring.messageList.noAssistantMessage', {
|
||||
defaultValue: '未记录助手回复',
|
||||
})}
|
||||
maxLines={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 flex-col gap-2 lg:items-end">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{turn.lastActivityAt.toLocaleString()}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex h-7 items-center rounded-md border px-2 text-xs font-medium',
|
||||
statusClass(turn.level),
|
||||
)}
|
||||
>
|
||||
{turn.level}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 lg:justify-end">
|
||||
<Metric
|
||||
icon={<Cpu className="h-3.5 w-3.5" />}
|
||||
label={`${turn.llmCalls.length} LLM`}
|
||||
/>
|
||||
<Metric
|
||||
icon={<Hash className="h-3.5 w-3.5" />}
|
||||
label={`${turn.totalTokens.toLocaleString()} tokens`}
|
||||
/>
|
||||
<Metric
|
||||
icon={<Clock className="h-3.5 w-3.5" />}
|
||||
label={formatDuration(turn.totalDuration)}
|
||||
/>
|
||||
{turn.errors.length > 0 && (
|
||||
<Metric
|
||||
icon={<AlertCircle className="h-3.5 w-3.5" />}
|
||||
label={`${turn.errors.length} errors`}
|
||||
tone="error"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="border-t bg-muted/40 p-3 sm:p-5">
|
||||
<div className="space-y-5 border-l-2 border-border pl-4 sm:pl-6">
|
||||
<div className="grid grid-cols-2 gap-2 lg:grid-cols-5">
|
||||
<MetaItem
|
||||
label={t('monitoring.messageList.platform', {
|
||||
defaultValue: '平台',
|
||||
})}
|
||||
value={turn.platform}
|
||||
/>
|
||||
<MetaItem
|
||||
label={t('monitoring.messageList.user', {
|
||||
defaultValue: '用户',
|
||||
})}
|
||||
value={turn.userName || turn.userId}
|
||||
/>
|
||||
<MetaItem
|
||||
label={t('monitoring.messageList.runner', {
|
||||
defaultValue: '执行器',
|
||||
})}
|
||||
value={turn.runnerName}
|
||||
/>
|
||||
<MetaItem
|
||||
label={t('monitoring.sessions.sessionId', {
|
||||
defaultValue: '会话 ID',
|
||||
})}
|
||||
value={turn.sessionId}
|
||||
/>
|
||||
<MetaItem
|
||||
label={t('monitoring.messageList.messageCount', {
|
||||
defaultValue: '消息数',
|
||||
})}
|
||||
value={String(turn.messages.length)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h4 className="mb-3 flex items-center gap-2 text-sm font-semibold text-foreground">
|
||||
<Bot className="h-4 w-4" />
|
||||
{t('monitoring.messageList.conversationTrace', {
|
||||
defaultValue: '消息链路',
|
||||
})}
|
||||
</h4>
|
||||
<div className="rounded-lg bg-background px-3 py-3">
|
||||
{turn.messages.map((message) => (
|
||||
<ExpandedMessage
|
||||
key={message.id}
|
||||
message={message}
|
||||
label={t(
|
||||
`monitoring.messageList.roles.${roleLabel(message)}`,
|
||||
{
|
||||
defaultValue:
|
||||
roleLabel(message) === 'assistant'
|
||||
? '助手'
|
||||
: roleLabel(message) === 'user'
|
||||
? '用户'
|
||||
: '消息',
|
||||
},
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h4 className="mb-3 flex items-center gap-2 text-sm font-semibold text-foreground">
|
||||
<Cpu className="h-4 w-4" />
|
||||
{t('monitoring.llmCalls.title', {
|
||||
defaultValue: 'LLM 调用',
|
||||
})}{' '}
|
||||
({turn.llmCalls.length})
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<MetaItem
|
||||
label={t('monitoring.llmCalls.totalTokens', {
|
||||
defaultValue: '总 Token',
|
||||
})}
|
||||
value={turn.totalTokens.toLocaleString()}
|
||||
/>
|
||||
<MetaItem
|
||||
label={t('monitoring.llmCalls.inputTokens', {
|
||||
defaultValue: '输入 Token',
|
||||
})}
|
||||
value={turn.inputTokens.toLocaleString()}
|
||||
/>
|
||||
<MetaItem
|
||||
label={t('monitoring.llmCalls.duration', {
|
||||
defaultValue: '耗时',
|
||||
})}
|
||||
value={formatDuration(turn.totalDuration)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 rounded-lg bg-background px-3 py-3">
|
||||
{turn.llmCalls.length > 0 ? (
|
||||
turn.llmCalls.map((call, index) => (
|
||||
<div
|
||||
key={call.id}
|
||||
className="border-t border-border/70 py-3 first:border-t-0 first:pt-0 last:pb-0"
|
||||
>
|
||||
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
#{index + 1} {call.modelName}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'rounded-md px-2 py-1 text-xs font-medium',
|
||||
call.status === 'success'
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-300'
|
||||
: 'bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300',
|
||||
)}
|
||||
>
|
||||
{call.status}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatDuration(call.duration)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-1 text-xs text-muted-foreground">
|
||||
<span>In: {call.tokens.input}</span>
|
||||
<span>Out: {call.tokens.output}</span>
|
||||
<span>Total: {call.tokens.total}</span>
|
||||
<span className="font-mono">
|
||||
ID: {shortId(call.id)}
|
||||
</span>
|
||||
</div>
|
||||
{call.errorMessage && (
|
||||
<div className="mt-2 whitespace-pre-wrap break-words text-xs text-red-600 dark:text-red-400">
|
||||
{call.errorMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="py-4 text-center text-sm text-muted-foreground">
|
||||
{t('monitoring.messageList.noLlmCalls', {
|
||||
defaultValue: '未记录模型调用',
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{turn.errors.length > 0 && (
|
||||
<section>
|
||||
<h4 className="mb-3 flex items-center gap-2 text-sm font-semibold text-red-700 dark:text-red-300">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
{t('monitoring.errors.title', {
|
||||
defaultValue: '错误日志',
|
||||
})}{' '}
|
||||
({turn.errors.length})
|
||||
</h4>
|
||||
<div className="rounded-lg bg-background px-3 py-3">
|
||||
{turn.errors.map((error) => (
|
||||
<div
|
||||
key={error.id}
|
||||
className="border-t border-red-200/80 py-3 first:border-t-0 first:pt-0 last:pb-0 dark:border-red-900"
|
||||
>
|
||||
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium text-red-700 dark:text-red-300">
|
||||
{error.errorType}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{error.timestamp.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap break-words text-sm text-red-600 dark:text-red-400">
|
||||
{error.errorMessage}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -145,8 +145,10 @@ export function useMonitoringData(filterState: FilterState) {
|
||||
level: string;
|
||||
platform?: string;
|
||||
user_id?: string;
|
||||
user_name?: string;
|
||||
runner_name?: string;
|
||||
variables?: string;
|
||||
role?: string;
|
||||
}) => ({
|
||||
id: msg.id,
|
||||
timestamp: parseUTCTimestamp(msg.timestamp),
|
||||
@@ -160,8 +162,10 @@ export function useMonitoringData(filterState: FilterState) {
|
||||
level: msg.level as 'info' | 'warning' | 'error' | 'debug',
|
||||
platform: msg.platform,
|
||||
userId: msg.user_id,
|
||||
userName: msg.user_name,
|
||||
runnerName: msg.runner_name,
|
||||
variables: msg.variables,
|
||||
role: msg.role,
|
||||
}),
|
||||
),
|
||||
llmCalls: llmCalls.map(
|
||||
@@ -179,6 +183,7 @@ export function useMonitoringData(filterState: FilterState) {
|
||||
bot_name: string;
|
||||
pipeline_id: string;
|
||||
pipeline_name: string;
|
||||
session_id?: string;
|
||||
error_message?: string;
|
||||
message_id?: string;
|
||||
}) => ({
|
||||
@@ -197,6 +202,7 @@ export function useMonitoringData(filterState: FilterState) {
|
||||
botName: call.bot_name,
|
||||
pipelineId: call.pipeline_id,
|
||||
pipelineName: call.pipeline_name,
|
||||
sessionId: call.session_id,
|
||||
errorMessage: call.error_message,
|
||||
messageId: call.message_id,
|
||||
}),
|
||||
@@ -317,6 +323,7 @@ export function useMonitoringData(filterState: FilterState) {
|
||||
botName: call.botName,
|
||||
pipelineId: call.pipelineId,
|
||||
pipelineName: call.pipelineName,
|
||||
sessionId: call.sessionId,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -18,60 +18,12 @@ import { ExportDropdown } from './components/ExportDropdown';
|
||||
import { useMonitoringFilters } from './hooks/useMonitoringFilters';
|
||||
import { useMonitoringData } from './hooks/useMonitoringData';
|
||||
import { useFeedbackData } from './hooks/useFeedbackData';
|
||||
import { MessageDetailsCard } from './components/MessageDetailsCard';
|
||||
import { MessageContentRenderer } from './components/MessageContentRenderer';
|
||||
import { ConversationTurnList } from './components/ConversationTurnList';
|
||||
import { FeedbackStatsCards } from './components/FeedbackCard';
|
||||
import { FeedbackList } from './components/FeedbackList';
|
||||
import { MessageDetails } from './types/monitoring';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { buildConversationTurns } from './utils/conversationTurns';
|
||||
import { LoadingSpinner, LoadingPage } from '@/components/ui/loading-spinner';
|
||||
|
||||
interface RawMessageData {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
bot_id: string;
|
||||
bot_name: string;
|
||||
pipeline_id: string;
|
||||
pipeline_name: string;
|
||||
message_content: string;
|
||||
session_id: string;
|
||||
status: string;
|
||||
level: string;
|
||||
platform: string;
|
||||
user_id: string;
|
||||
runner_name: string;
|
||||
variables: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface RawLLMCallData {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
model_name: string;
|
||||
status: string;
|
||||
duration: number;
|
||||
error_message: string | null;
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
total_tokens: number;
|
||||
}
|
||||
|
||||
interface RawLLMStatsData {
|
||||
total_calls: number;
|
||||
total_input_tokens: number;
|
||||
total_output_tokens: number;
|
||||
total_tokens: number;
|
||||
total_duration_ms: number;
|
||||
average_duration_ms: number;
|
||||
}
|
||||
|
||||
interface RawErrorData {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
error_type: string;
|
||||
error_message: string;
|
||||
stack_trace: string | null;
|
||||
}
|
||||
|
||||
function MonitoringPageContent() {
|
||||
const { t } = useTranslation();
|
||||
const { filterState, setSelectedBots, setSelectedPipelines, setTimeRange } =
|
||||
@@ -146,115 +98,36 @@ function MonitoringPageContent() {
|
||||
setFeedbackRefreshKey((k) => k + 1);
|
||||
}, [refetch]);
|
||||
|
||||
const [expandedMessageId, setExpandedMessageId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [messageDetails, setMessageDetails] = useState<
|
||||
Record<string, MessageDetails>
|
||||
>({});
|
||||
const [loadingDetails, setLoadingDetails] = useState<Record<string, boolean>>(
|
||||
{},
|
||||
const conversationTurns = useMemo(
|
||||
() =>
|
||||
buildConversationTurns(
|
||||
data?.messages || [],
|
||||
data?.llmCalls || [],
|
||||
data?.errors || [],
|
||||
),
|
||||
[data?.messages, data?.llmCalls, data?.errors],
|
||||
);
|
||||
|
||||
// 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>('messages');
|
||||
|
||||
// Function to jump to a message record
|
||||
const jumpToMessage = async (messageId: string) => {
|
||||
const jumpToMessage = (messageId: string) => {
|
||||
setActiveTab('messages');
|
||||
// Small delay to ensure tab switch completes
|
||||
setTimeout(() => {
|
||||
toggleMessageExpand(messageId);
|
||||
const turn = conversationTurns.find((item) =>
|
||||
item.messages.some((message) => message.id === messageId),
|
||||
);
|
||||
setExpandedTurnId(turn?.id ?? messageId);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const toggleMessageExpand = async (messageId: string) => {
|
||||
if (expandedMessageId === messageId) {
|
||||
// Collapse
|
||||
setExpandedMessageId(null);
|
||||
} else {
|
||||
// Expand
|
||||
setExpandedMessageId(messageId);
|
||||
|
||||
// Fetch details if not already loaded
|
||||
if (!messageDetails[messageId]) {
|
||||
setLoadingDetails({ ...loadingDetails, [messageId]: true });
|
||||
try {
|
||||
// httpClient.get() returns the inner data directly (response.data.data)
|
||||
const result = await httpClient.get<{
|
||||
message_id: string;
|
||||
found: boolean;
|
||||
message: RawMessageData | null;
|
||||
llm_calls: RawLLMCallData[];
|
||||
llm_stats: RawLLMStatsData;
|
||||
errors: RawErrorData[];
|
||||
}>(`/api/v1/monitoring/messages/${messageId}/details`);
|
||||
|
||||
if (result) {
|
||||
setMessageDetails((prev) => ({
|
||||
...prev,
|
||||
[messageId]: {
|
||||
messageId: result.message_id,
|
||||
found: result.found,
|
||||
message: result.message
|
||||
? {
|
||||
id: result.message.id,
|
||||
timestamp: new Date(result.message.timestamp),
|
||||
botId: result.message.bot_id,
|
||||
botName: result.message.bot_name,
|
||||
pipelineId: result.message.pipeline_id,
|
||||
pipelineName: result.message.pipeline_name,
|
||||
messageContent: result.message.message_content,
|
||||
sessionId: result.message.session_id,
|
||||
status: result.message.status,
|
||||
level: result.message.level,
|
||||
platform: result.message.platform,
|
||||
userId: result.message.user_id,
|
||||
runnerName: result.message.runner_name,
|
||||
variables: result.message.variables,
|
||||
}
|
||||
: undefined,
|
||||
llmCalls: result.llm_calls.map((call: RawLLMCallData) => ({
|
||||
id: call.id,
|
||||
timestamp: new Date(call.timestamp),
|
||||
modelName: call.model_name,
|
||||
status: call.status,
|
||||
duration: call.duration,
|
||||
errorMessage: call.error_message,
|
||||
tokens: {
|
||||
input: call.input_tokens || 0,
|
||||
output: call.output_tokens || 0,
|
||||
total: call.total_tokens || 0,
|
||||
},
|
||||
})),
|
||||
errors: result.errors.map((error: RawErrorData) => ({
|
||||
id: error.id,
|
||||
timestamp: new Date(error.timestamp),
|
||||
errorType: error.error_type,
|
||||
errorMessage: error.error_message,
|
||||
stackTrace: error.stack_trace,
|
||||
})),
|
||||
llmStats: {
|
||||
totalCalls: result.llm_stats.total_calls,
|
||||
totalInputTokens: result.llm_stats.total_input_tokens,
|
||||
totalOutputTokens: result.llm_stats.total_output_tokens,
|
||||
totalTokens: result.llm_stats.total_tokens,
|
||||
totalDurationMs: result.llm_stats.total_duration_ms,
|
||||
averageDurationMs: result.llm_stats.average_duration_ms,
|
||||
},
|
||||
} as MessageDetails,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch message details:', error);
|
||||
} finally {
|
||||
setLoadingDetails({ ...loadingDetails, [messageId]: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
const toggleTurnExpand = (turnId: string) => {
|
||||
setExpandedTurnId((current) => (current === turnId ? null : turnId));
|
||||
};
|
||||
|
||||
const toggleErrorExpand = (errorId: string) => {
|
||||
@@ -342,127 +215,22 @@ function MonitoringPageContent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading &&
|
||||
data &&
|
||||
data.messages &&
|
||||
data.messages.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
{data.messages
|
||||
.filter((msg) => {
|
||||
// Filter out messages with empty content
|
||||
const content = msg.messageContent?.trim();
|
||||
return (
|
||||
content && content !== '[]' && content !== '""'
|
||||
);
|
||||
})
|
||||
.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className="border rounded-xl overflow-hidden transition-all duration-200"
|
||||
>
|
||||
{/* Message Header - Always Visible */}
|
||||
<div
|
||||
className="p-3 cursor-pointer hover:bg-accent transition-colors sm:p-5"
|
||||
onClick={() => toggleMessageExpand(msg.id)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start flex-1">
|
||||
{/* Expand Icon */}
|
||||
<div className="mr-3 mt-0.5">
|
||||
{expandedMessageId === msg.id ? (
|
||||
<ChevronDown className="w-5 h-5 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="w-5 h-5 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
{!loading && data && conversationTurns.length > 0 && (
|
||||
<ConversationTurnList
|
||||
turns={conversationTurns}
|
||||
expandedTurnId={expandedTurnId}
|
||||
onToggleTurn={toggleTurnExpand}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Message Info */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
ID: {msg.id}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="font-medium text-sm text-foreground">
|
||||
{msg.botName}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
→
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{msg.pipelineName}
|
||||
</span>
|
||||
{msg.runnerName && (
|
||||
<>
|
||||
<span className="text-muted-foreground">
|
||||
→
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{msg.runnerName}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-base text-foreground">
|
||||
<MessageContentRenderer
|
||||
content={msg.messageContent}
|
||||
maxLines={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status and Timestamp */}
|
||||
<div className="flex flex-col items-end gap-2 ml-4">
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
{msg.timestamp.toLocaleString()}
|
||||
</span>
|
||||
<span
|
||||
className={`text-xs px-2 py-1 rounded ${
|
||||
msg.level === 'error'
|
||||
? 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
||||
: msg.level === 'warning'
|
||||
? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
|
||||
: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||
}`}
|
||||
>
|
||||
{msg.level}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Details */}
|
||||
{expandedMessageId === msg.id && (
|
||||
<div className="border-t p-4 bg-muted">
|
||||
{loadingDetails[msg.id] && (
|
||||
<div className="py-4 flex justify-center">
|
||||
<LoadingSpinner size="sm" text="" />
|
||||
</div>
|
||||
)}
|
||||
{!loadingDetails[msg.id] &&
|
||||
messageDetails[msg.id] && (
|
||||
<MessageDetailsCard
|
||||
details={messageDetails[msg.id]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!loading && (!data || conversationTurns.length === 0) && (
|
||||
<div className="flex flex-col items-center justify-center text-muted-foreground py-16 gap-2">
|
||||
<MessageSquare className="h-[3rem] w-[3rem]" />
|
||||
<div className="text-sm">
|
||||
{t('monitoring.messageList.noMessages')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading &&
|
||||
(!data || !data.messages || data.messages.length === 0) && (
|
||||
<div className="flex flex-col items-center justify-center text-muted-foreground py-16 gap-2">
|
||||
<MessageSquare className="h-[3rem] w-[3rem]" />
|
||||
<div className="text-sm">
|
||||
{t('monitoring.messageList.noMessages')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@@ -11,8 +11,10 @@ export interface MonitoringMessage {
|
||||
level: 'info' | 'warning' | 'error' | 'debug';
|
||||
platform?: string;
|
||||
userId?: string;
|
||||
userName?: string;
|
||||
runnerName?: string;
|
||||
variables?: string;
|
||||
role?: 'user' | 'assistant' | string;
|
||||
}
|
||||
|
||||
export interface LLMCall {
|
||||
@@ -31,6 +33,7 @@ export interface LLMCall {
|
||||
botName: string;
|
||||
pipelineId: string;
|
||||
pipelineName: string;
|
||||
sessionId?: string;
|
||||
errorMessage?: string;
|
||||
messageId?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
import { ErrorLog, LLMCall, MonitoringMessage } from '../types/monitoring';
|
||||
|
||||
type MessageRole = 'user' | 'assistant' | 'unknown';
|
||||
|
||||
export interface ConversationTurn {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
startedAt: Date;
|
||||
lastActivityAt: Date;
|
||||
botId: string;
|
||||
botName: string;
|
||||
pipelineId: string;
|
||||
pipelineName: string;
|
||||
runnerName?: string;
|
||||
platform?: string;
|
||||
userId?: string;
|
||||
userName?: string;
|
||||
userMessage?: MonitoringMessage;
|
||||
assistantMessages: MonitoringMessage[];
|
||||
messages: MonitoringMessage[];
|
||||
llmCalls: LLMCall[];
|
||||
errors: ErrorLog[];
|
||||
status: 'success' | 'error' | 'pending';
|
||||
level: 'info' | 'warning' | 'error' | 'debug';
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
totalTokens: number;
|
||||
totalDuration: number;
|
||||
}
|
||||
|
||||
function normalizeRole(
|
||||
message: MonitoringMessage,
|
||||
llmMessageIds: Set<string>,
|
||||
): MessageRole {
|
||||
const role = message.role?.toLowerCase();
|
||||
|
||||
if (role === 'user' || role === 'assistant') {
|
||||
return role;
|
||||
}
|
||||
|
||||
if (llmMessageIds.has(message.id)) {
|
||||
return 'user';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
export function hasRenderableMessageContent(content?: string): boolean {
|
||||
const trimmed = content?.trim();
|
||||
|
||||
if (!trimmed || trimmed === '[]' || trimmed === '""') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
|
||||
if (typeof parsed === 'string') {
|
||||
return parsed.trim().length > 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.some(
|
||||
(component) =>
|
||||
typeof component !== 'object' ||
|
||||
component === null ||
|
||||
component.type !== 'Source',
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function createTurn(message: MonitoringMessage): ConversationTurn {
|
||||
return {
|
||||
id: message.id,
|
||||
sessionId: message.sessionId,
|
||||
startedAt: message.timestamp,
|
||||
lastActivityAt: message.timestamp,
|
||||
botId: message.botId,
|
||||
botName: message.botName,
|
||||
pipelineId: message.pipelineId,
|
||||
pipelineName: message.pipelineName,
|
||||
runnerName: message.runnerName,
|
||||
platform: message.platform,
|
||||
userId: message.userId,
|
||||
userName: message.userName,
|
||||
assistantMessages: [],
|
||||
messages: [],
|
||||
llmCalls: [],
|
||||
errors: [],
|
||||
status: message.status,
|
||||
level: message.level,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalTokens: 0,
|
||||
totalDuration: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function updateTurnActivity(turn: ConversationTurn, timestamp: Date) {
|
||||
if (timestamp.getTime() > turn.lastActivityAt.getTime()) {
|
||||
turn.lastActivityAt = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
function addMessageToTurn(
|
||||
turn: ConversationTurn,
|
||||
message: MonitoringMessage,
|
||||
role: MessageRole,
|
||||
) {
|
||||
turn.messages.push(message);
|
||||
updateTurnActivity(turn, message.timestamp);
|
||||
|
||||
if (message.level === 'error') {
|
||||
turn.level = 'error';
|
||||
} else if (message.level === 'warning' && turn.level !== 'error') {
|
||||
turn.level = 'warning';
|
||||
}
|
||||
|
||||
if (message.status === 'error') {
|
||||
turn.status = 'error';
|
||||
} else if (message.status === 'pending' && turn.status !== 'error') {
|
||||
turn.status = 'pending';
|
||||
}
|
||||
|
||||
if (role === 'assistant') {
|
||||
turn.assistantMessages.push(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!turn.userMessage) {
|
||||
turn.userMessage = message;
|
||||
turn.userId = message.userId ?? turn.userId;
|
||||
turn.userName = message.userName ?? turn.userName;
|
||||
return;
|
||||
}
|
||||
|
||||
turn.assistantMessages.push(message);
|
||||
}
|
||||
|
||||
function findTurnBySessionTime(
|
||||
sessionTurns: Map<string, ConversationTurn[]>,
|
||||
sessionId: string | undefined,
|
||||
timestamp: Date,
|
||||
): ConversationTurn | undefined {
|
||||
if (!sessionId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const turns = sessionTurns.get(sessionId);
|
||||
if (!turns?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let nearest = turns[0];
|
||||
const targetTime = timestamp.getTime();
|
||||
|
||||
for (const turn of turns) {
|
||||
if (turn.startedAt.getTime() <= targetTime) {
|
||||
nearest = turn;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
|
||||
export function buildConversationTurns(
|
||||
messages: MonitoringMessage[],
|
||||
llmCalls: LLMCall[],
|
||||
errors: ErrorLog[],
|
||||
): ConversationTurn[] {
|
||||
const llmMessageIds = new Set(
|
||||
llmCalls
|
||||
.map((call) => call.messageId)
|
||||
.filter((messageId): messageId is string => Boolean(messageId)),
|
||||
);
|
||||
const visibleMessages = messages
|
||||
.filter((message) => hasRenderableMessageContent(message.messageContent))
|
||||
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
||||
|
||||
const sessionTurns = new Map<string, ConversationTurn[]>();
|
||||
const lastTurnBySession = new Map<string, ConversationTurn>();
|
||||
const messageIdToTurn = new Map<string, ConversationTurn>();
|
||||
|
||||
for (const message of visibleMessages) {
|
||||
const role = normalizeRole(message, llmMessageIds);
|
||||
const previousTurn = lastTurnBySession.get(message.sessionId);
|
||||
const shouldStartTurn = role === 'user' || !previousTurn;
|
||||
const turn = shouldStartTurn ? createTurn(message) : previousTurn;
|
||||
|
||||
if (shouldStartTurn) {
|
||||
const turns = sessionTurns.get(message.sessionId) ?? [];
|
||||
turns.push(turn);
|
||||
sessionTurns.set(message.sessionId, turns);
|
||||
lastTurnBySession.set(message.sessionId, turn);
|
||||
}
|
||||
|
||||
addMessageToTurn(turn, message, role);
|
||||
messageIdToTurn.set(message.id, turn);
|
||||
}
|
||||
|
||||
const allTurns = Array.from(sessionTurns.values()).flat();
|
||||
|
||||
for (const call of llmCalls) {
|
||||
const turn =
|
||||
(call.messageId ? messageIdToTurn.get(call.messageId) : undefined) ??
|
||||
findTurnBySessionTime(sessionTurns, call.sessionId, call.timestamp);
|
||||
|
||||
if (!turn) {
|
||||
continue;
|
||||
}
|
||||
|
||||
turn.llmCalls.push(call);
|
||||
turn.inputTokens += call.tokens.input;
|
||||
turn.outputTokens += call.tokens.output;
|
||||
turn.totalTokens += call.tokens.total;
|
||||
turn.totalDuration += call.duration;
|
||||
updateTurnActivity(turn, call.timestamp);
|
||||
|
||||
if (call.status === 'error') {
|
||||
turn.status = 'error';
|
||||
turn.level = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
for (const error of errors) {
|
||||
const turn =
|
||||
(error.messageId ? messageIdToTurn.get(error.messageId) : undefined) ??
|
||||
findTurnBySessionTime(sessionTurns, error.sessionId, error.timestamp);
|
||||
|
||||
if (!turn) {
|
||||
continue;
|
||||
}
|
||||
|
||||
turn.errors.push(error);
|
||||
turn.status = 'error';
|
||||
turn.level = 'error';
|
||||
updateTurnActivity(turn, error.timestamp);
|
||||
}
|
||||
|
||||
for (const turn of allTurns) {
|
||||
turn.messages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
||||
turn.assistantMessages.sort(
|
||||
(a, b) => a.timestamp.getTime() - b.timestamp.getTime(),
|
||||
);
|
||||
turn.llmCalls.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
||||
turn.errors.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
||||
}
|
||||
|
||||
return allTurns.sort(
|
||||
(a, b) => b.lastActivityAt.getTime() - a.lastActivityAt.getTime(),
|
||||
);
|
||||
}
|
||||
@@ -1329,6 +1329,20 @@ const enUS = {
|
||||
level: 'Level',
|
||||
runner: 'Runner',
|
||||
viewConversation: 'View Conversation',
|
||||
turns: '{{count}} conversation turns',
|
||||
userMessage: 'User',
|
||||
noUserMessage: 'No user input recorded',
|
||||
assistantMessage: 'Assistant',
|
||||
assistantMessageCount: 'Assistant +{{count}}',
|
||||
noAssistantMessage: 'No assistant reply recorded',
|
||||
messageCount: 'Messages',
|
||||
conversationTrace: 'Conversation Trace',
|
||||
noLlmCalls: 'No model calls recorded',
|
||||
roles: {
|
||||
user: 'User',
|
||||
assistant: 'Assistant',
|
||||
message: 'Message',
|
||||
},
|
||||
},
|
||||
llmCalls: {
|
||||
title: 'LLM Calls',
|
||||
|
||||
@@ -1363,6 +1363,20 @@ const esES = {
|
||||
level: 'Nivel',
|
||||
runner: 'Ejecutor',
|
||||
viewConversation: 'Ver conversación',
|
||||
turns: '{{count}} turnos de conversación',
|
||||
userMessage: 'Usuario',
|
||||
noUserMessage: 'No se registró entrada del usuario',
|
||||
assistantMessage: 'Asistente',
|
||||
assistantMessageCount: 'Asistente +{{count}}',
|
||||
noAssistantMessage: 'No se registró respuesta del asistente',
|
||||
messageCount: 'Mensajes',
|
||||
conversationTrace: 'Flujo de conversación',
|
||||
noLlmCalls: 'No se registraron llamadas al modelo',
|
||||
roles: {
|
||||
user: 'Usuario',
|
||||
assistant: 'Asistente',
|
||||
message: 'Mensaje',
|
||||
},
|
||||
},
|
||||
llmCalls: {
|
||||
title: 'Llamadas LLM',
|
||||
|
||||
@@ -1336,6 +1336,20 @@ const jaJP = {
|
||||
level: 'レベル',
|
||||
runner: 'ランナー',
|
||||
viewConversation: '会話詳細を表示',
|
||||
turns: '{{count}} 会話ターン',
|
||||
userMessage: 'ユーザー',
|
||||
noUserMessage: 'ユーザー入力は記録されていません',
|
||||
assistantMessage: 'アシスタント',
|
||||
assistantMessageCount: 'アシスタント +{{count}}',
|
||||
noAssistantMessage: 'アシスタントの返信は記録されていません',
|
||||
messageCount: 'メッセージ数',
|
||||
conversationTrace: '会話トレース',
|
||||
noLlmCalls: 'モデル呼び出しは記録されていません',
|
||||
roles: {
|
||||
user: 'ユーザー',
|
||||
assistant: 'アシスタント',
|
||||
message: 'メッセージ',
|
||||
},
|
||||
},
|
||||
llmCalls: {
|
||||
title: 'LLM呼び出し',
|
||||
|
||||
@@ -1339,6 +1339,20 @@ const ruRU = {
|
||||
level: 'Уровень',
|
||||
runner: 'Обработчик',
|
||||
viewConversation: 'Просмотр диалога',
|
||||
turns: '{{count}} диалоговых ходов',
|
||||
userMessage: 'Пользователь',
|
||||
noUserMessage: 'Ввод пользователя не записан',
|
||||
assistantMessage: 'Ассистент',
|
||||
assistantMessageCount: 'Ассистент +{{count}}',
|
||||
noAssistantMessage: 'Ответ ассистента не записан',
|
||||
messageCount: 'Сообщения',
|
||||
conversationTrace: 'Ход диалога',
|
||||
noLlmCalls: 'Вызовы модели не записаны',
|
||||
roles: {
|
||||
user: 'Пользователь',
|
||||
assistant: 'Ассистент',
|
||||
message: 'Сообщение',
|
||||
},
|
||||
},
|
||||
llmCalls: {
|
||||
title: 'Вызовы LLM',
|
||||
|
||||
@@ -1307,6 +1307,20 @@ const thTH = {
|
||||
level: 'ระดับ',
|
||||
runner: 'ตัวประมวลผล',
|
||||
viewConversation: 'ดูการสนทนา',
|
||||
turns: '{{count}} รอบการสนทนา',
|
||||
userMessage: 'ผู้ใช้',
|
||||
noUserMessage: 'ยังไม่มีการบันทึกข้อความจากผู้ใช้',
|
||||
assistantMessage: 'ผู้ช่วย',
|
||||
assistantMessageCount: 'ผู้ช่วย +{{count}}',
|
||||
noAssistantMessage: 'ยังไม่มีการบันทึกคำตอบจากผู้ช่วย',
|
||||
messageCount: 'จำนวนข้อความ',
|
||||
conversationTrace: 'ลำดับการสนทนา',
|
||||
noLlmCalls: 'ยังไม่มีการบันทึกการเรียกโมเดล',
|
||||
roles: {
|
||||
user: 'ผู้ใช้',
|
||||
assistant: 'ผู้ช่วย',
|
||||
message: 'ข้อความ',
|
||||
},
|
||||
},
|
||||
llmCalls: {
|
||||
title: 'การเรียก LLM',
|
||||
|
||||
@@ -1332,6 +1332,20 @@ const viVN = {
|
||||
level: 'Mức',
|
||||
runner: 'Trình chạy',
|
||||
viewConversation: 'Xem cuộc trò chuyện',
|
||||
turns: '{{count}} lượt hội thoại',
|
||||
userMessage: 'Người dùng',
|
||||
noUserMessage: 'Chưa ghi nhận đầu vào người dùng',
|
||||
assistantMessage: 'Trợ lý',
|
||||
assistantMessageCount: 'Trợ lý +{{count}}',
|
||||
noAssistantMessage: 'Chưa ghi nhận phản hồi của trợ lý',
|
||||
messageCount: 'Số tin nhắn',
|
||||
conversationTrace: 'Luồng hội thoại',
|
||||
noLlmCalls: 'Chưa ghi nhận lệnh gọi mô hình',
|
||||
roles: {
|
||||
user: 'Người dùng',
|
||||
assistant: 'Trợ lý',
|
||||
message: 'Tin nhắn',
|
||||
},
|
||||
},
|
||||
llmCalls: {
|
||||
title: 'Cuộc gọi LLM',
|
||||
|
||||
@@ -1266,6 +1266,20 @@ const zhHans = {
|
||||
level: '级别',
|
||||
runner: '执行器',
|
||||
viewConversation: '显示对话详情',
|
||||
turns: '{{count}} 轮对话',
|
||||
userMessage: '用户',
|
||||
noUserMessage: '未记录用户输入',
|
||||
assistantMessage: '助手',
|
||||
assistantMessageCount: '助手 +{{count}}',
|
||||
noAssistantMessage: '未记录助手回复',
|
||||
messageCount: '消息数',
|
||||
conversationTrace: '消息链路',
|
||||
noLlmCalls: '未记录模型调用',
|
||||
roles: {
|
||||
user: '用户',
|
||||
assistant: '助手',
|
||||
message: '消息',
|
||||
},
|
||||
},
|
||||
llmCalls: {
|
||||
title: 'LLM调用',
|
||||
|
||||
@@ -1264,6 +1264,20 @@ const zhHant = {
|
||||
level: '級別',
|
||||
runner: '執行器',
|
||||
viewConversation: '顯示對話詳情',
|
||||
turns: '{{count}} 輪對話',
|
||||
userMessage: '使用者',
|
||||
noUserMessage: '未記錄使用者輸入',
|
||||
assistantMessage: '助手',
|
||||
assistantMessageCount: '助手 +{{count}}',
|
||||
noAssistantMessage: '未記錄助手回覆',
|
||||
messageCount: '訊息數',
|
||||
conversationTrace: '訊息鏈路',
|
||||
noLlmCalls: '未記錄模型呼叫',
|
||||
roles: {
|
||||
user: '使用者',
|
||||
assistant: '助手',
|
||||
message: '訊息',
|
||||
},
|
||||
},
|
||||
llmCalls: {
|
||||
title: 'LLM呼叫',
|
||||
|
||||
Reference in New Issue
Block a user