Merge remote-tracking branch 'origin/master' into temp-update

# Conflicts:
#	web/pnpm-lock.yaml
This commit is contained in:
Typer_Body
2026-04-02 01:18:38 +08:00
61 changed files with 5143 additions and 1267 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
# Debug LangBot Frontend
Please refer to the [Development Guide](https://docs.langbot.app/en/develop/dev-config.html) for more information.
Please refer to the [Development Guide](https://link.langbot.app/en/docs/dev-config) for more information.
+3276 -1099
View File
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import i18n from 'i18next';
import {
IChooseAdapterEntity,
IPipelineEntity,
@@ -13,6 +14,8 @@ import { UUID } from 'uuidjs';
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import { httpClient } from '@/app/infra/http/HttpClient';
import { Bot } from '@/app/infra/entities/api';
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
import { ExternalLink } from 'lucide-react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
@@ -102,6 +105,9 @@ export default function BotForm({
const [adapterDescriptionList, setAdapterDescriptionList] = useState<
Record<string, string>
>({});
const [adapterHelpLinks, setAdapterHelpLinks] = useState<
Record<string, Record<string, string>>
>({});
const [pipelineNameList, setPipelineNameList] = useState<IPipelineEntity[]>(
[],
@@ -209,6 +215,18 @@ export default function BotForm({
),
);
setAdapterHelpLinks(
adaptersRes.adapters.reduce(
(acc, item) => {
if (item.spec.help_links) {
acc[item.name] = item.spec.help_links;
}
return acc;
},
{} as Record<string, Record<string, string>>,
),
);
adaptersRes.adapters.forEach((rawAdapter) => {
adapterNameToDynamicConfigMap.set(
rawAdapter.name,
@@ -469,59 +487,81 @@ export default function BotForm({
<span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<Select
onValueChange={(value) => {
field.onChange(value);
handleAdapterSelect(value);
}}
value={field.value}
>
<SelectTrigger className="w-[240px]">
{field.value ? (
<div className="flex items-center gap-2">
<img
src={httpClient.getAdapterIconURL(field.value)}
alt=""
className="h-5 w-5 rounded"
<div className="flex items-center gap-2">
<Select
onValueChange={(value) => {
field.onChange(value);
handleAdapterSelect(value);
}}
value={field.value}
>
<SelectTrigger className="w-[240px]">
{field.value ? (
<div className="flex items-center gap-2">
<img
src={httpClient.getAdapterIconURL(field.value)}
alt=""
className="h-5 w-5 rounded"
/>
<span>
{adapterNameList.find(
(a) => a.value === field.value,
)?.label ?? field.value}
</span>
</div>
) : (
<SelectValue
placeholder={t('bots.selectAdapter')}
/>
<span>
{adapterNameList.find(
(a) => a.value === field.value,
)?.label ?? field.value}
</span>
</div>
) : (
<SelectValue placeholder={t('bots.selectAdapter')} />
)}
</SelectTrigger>
<SelectContent>
{groupedAdapters.map((group) => (
<SelectGroup
key={group.categoryId ?? 'uncategorized'}
>
{group.categoryId && (
<SelectLabel>
{getCategoryLabel(t, group.categoryId)}
</SelectLabel>
)}
{group.items.map((item) => (
<SelectItem key={item.value} value={item.value}>
<div className="flex items-center gap-2">
<img
src={httpClient.getAdapterIconURL(
item.value,
)}
alt=""
className="h-5 w-5 rounded"
/>
<span>{item.label}</span>
</div>
</SelectItem>
))}
</SelectGroup>
))}
</SelectContent>
</Select>
)}
</SelectTrigger>
<SelectContent>
{groupedAdapters.map((group) => (
<SelectGroup
key={group.categoryId ?? 'uncategorized'}
>
{group.categoryId && (
<SelectLabel>
{getCategoryLabel(t, group.categoryId)}
</SelectLabel>
)}
{group.items.map((item) => (
<SelectItem key={item.value} value={item.value}>
<div className="flex items-center gap-2">
<img
src={httpClient.getAdapterIconURL(
item.value,
)}
alt=""
className="h-5 w-5 rounded"
/>
<span>{item.label}</span>
</div>
</SelectItem>
))}
</SelectGroup>
))}
</SelectContent>
</Select>
{currentAdapter &&
(() => {
const docUrl = getAdapterDocUrl(
adapterHelpLinks[currentAdapter],
i18n.language,
);
return docUrl ? (
<a
href={docUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex shrink-0 items-center gap-1 text-xs text-primary hover:underline"
>
{t('bots.viewAdapterDocs')}
<ExternalLink className="h-3 w-3" />
</a>
) : null;
})()}
</div>
</FormControl>
{currentAdapter && adapterDescriptionList[currentAdapter] && (
<FormDescription>
@@ -1423,12 +1423,12 @@ export default function HomeSidebar({
localStorage.getItem('langbot_language');
if (language === 'zh-Hans' || language === 'zh-Hant') {
window.open(
'https://docs.langbot.app/zh/insight/guide',
'https://link.langbot.app/zh/docs/guide',
'_blank',
);
} else {
window.open(
'https://docs.langbot.app/en/insight/guide',
'https://link.langbot.app/en/docs/guide',
'_blank',
);
}
@@ -67,9 +67,9 @@ export const sidebarConfigList = [
route: '/home/bots',
description: t('bots.description'),
helpLink: {
en_US: 'https://docs.langbot.app/en/usage/platforms/readme',
zh_Hans: 'https://docs.langbot.app/zh/usage/platforms/readme',
ja_JP: 'https://docs.langbot.app/ja/usage/platforms/readme',
en_US: 'https://link.langbot.app/en/docs/platforms',
zh_Hans: 'https://link.langbot.app/zh/docs/platforms',
ja_JP: 'https://link.langbot.app/ja/docs/platforms',
},
section: 'home',
}),
@@ -89,9 +89,9 @@ export const sidebarConfigList = [
route: '/home/pipelines',
description: t('pipelines.description'),
helpLink: {
en_US: 'https://docs.langbot.app/en/usage/pipelines/readme',
zh_Hans: 'https://docs.langbot.app/zh/usage/pipelines/readme',
ja_JP: 'https://docs.langbot.app/ja/usage/pipelines/readme',
en_US: 'https://link.langbot.app/en/docs/pipelines',
zh_Hans: 'https://link.langbot.app/zh/docs/pipelines',
ja_JP: 'https://link.langbot.app/ja/docs/pipelines',
},
section: 'home',
}),
@@ -111,9 +111,9 @@ export const sidebarConfigList = [
route: '/home/knowledge',
description: t('knowledge.description'),
helpLink: {
en_US: 'https://docs.langbot.app/en/usage/knowledge/readme',
zh_Hans: 'https://docs.langbot.app/zh/usage/knowledge/readme',
ja_JP: 'https://docs.langbot.app/ja/usage/knowledge/readme',
en_US: 'https://link.langbot.app/en/docs/knowledge',
zh_Hans: 'https://link.langbot.app/zh/docs/knowledge',
ja_JP: 'https://link.langbot.app/ja/docs/knowledge',
},
section: 'home',
}),
@@ -135,9 +135,9 @@ export const sidebarConfigList = [
route: '/home/plugins',
description: t('plugins.description'),
helpLink: {
en_US: 'https://docs.langbot.app/en/usage/plugin/plugin-intro',
zh_Hans: 'https://docs.langbot.app/zh/usage/plugin/plugin-intro',
ja_JP: 'https://docs.langbot.app/ja/usage/plugin/plugin-intro',
en_US: 'https://link.langbot.app/en/docs/plugins',
zh_Hans: 'https://link.langbot.app/zh/docs/plugins',
ja_JP: 'https://link.langbot.app/ja/docs/plugins',
},
section: 'extensions',
}),
@@ -157,9 +157,9 @@ export const sidebarConfigList = [
route: '/home/market',
description: t('plugins.description'),
helpLink: {
en_US: 'https://docs.langbot.app/en/usage/plugin/plugin-intro',
zh_Hans: 'https://docs.langbot.app/zh/usage/plugin/plugin-intro',
ja_JP: 'https://docs.langbot.app/ja/usage/plugin/plugin-intro',
en_US: 'https://link.langbot.app/en/docs/plugins',
zh_Hans: 'https://link.langbot.app/zh/docs/plugins',
ja_JP: 'https://link.langbot.app/ja/docs/plugins',
},
section: 'extensions',
}),
@@ -36,11 +36,11 @@ export default function NewVersionDialog({
const getUpdateDocsUrl = () => {
const language = i18n.language;
if (language === 'zh-Hans' || language === 'zh-Hant') {
return 'https://docs.langbot.app/zh/deploy/update';
return 'https://link.langbot.app/zh/docs/update';
} else if (language === 'ja-JP') {
return 'https://docs.langbot.app/ja/deploy/update';
return 'https://link.langbot.app/ja/docs/update';
} else {
return 'https://docs.langbot.app/en/deploy/update';
return 'https://link.langbot.app/en/docs/update';
}
};
@@ -0,0 +1,187 @@
'use client';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
ThumbsUp,
ThumbsDown,
TrendingUp,
TrendingDown,
Minus,
} from 'lucide-react';
interface FeedbackCardProps {
title: string;
value: number | string;
subtitle?: string;
icon: React.ReactNode;
trend?: {
value: number;
direction: 'up' | 'down' | 'neutral';
};
variant?: 'default' | 'success' | 'warning' | 'danger';
loading?: boolean;
}
export function FeedbackCard({
title,
value,
subtitle,
icon,
trend,
variant = 'default',
loading = false,
}: FeedbackCardProps) {
const variantStyles = {
default: 'bg-white dark:bg-[#2a2a2e] border-gray-200 dark:border-gray-700',
success:
'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800',
warning:
'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800',
danger: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800',
};
const iconStyles = {
default: 'text-gray-500 dark:text-gray-400',
success: 'text-green-500 dark:text-green-400',
warning: 'text-yellow-500 dark:text-yellow-400',
danger: 'text-red-500 dark:text-red-400',
};
const trendStyles = {
up: 'text-green-500',
down: 'text-red-500',
neutral: 'text-gray-500',
};
if (loading) {
return (
<div
className={`p-6 rounded-xl border shadow-sm ${variantStyles.default} animate-pulse`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2" />
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-16 mb-1" />
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-24" />
</div>
<div className="w-10 h-10 bg-gray-200 dark:bg-gray-700 rounded-lg" />
</div>
</div>
);
}
return (
<div
className={`p-6 rounded-xl border shadow-sm ${variantStyles[variant]}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">
{title}
</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{value}
</p>
{subtitle && (
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
{subtitle}
</p>
)}
{trend && (
<div
className={`flex items-center mt-2 text-sm ${trendStyles[trend.direction]}`}
>
{trend.direction === 'up' && (
<TrendingUp className="w-4 h-4 mr-1" />
)}
{trend.direction === 'down' && (
<TrendingDown className="w-4 h-4 mr-1" />
)}
{trend.direction === 'neutral' && (
<Minus className="w-4 h-4 mr-1" />
)}
<span>
{trend.value > 0 ? '+' : ''}
{trend.value}%
</span>
</div>
)}
</div>
<div
className={`p-3 rounded-lg bg-gray-100 dark:bg-gray-800 ${iconStyles[variant]}`}
>
{icon}
</div>
</div>
</div>
);
}
interface FeedbackStatsProps {
stats: {
totalFeedback: number;
totalLikes: number;
totalDislikes: number;
satisfactionRate: number;
} | null;
loading?: boolean;
}
export function FeedbackStatsCards({ stats, loading }: FeedbackStatsProps) {
const { t } = useTranslation();
const cards = [
{
title: t('monitoring.feedback.totalFeedback'),
value: stats?.totalFeedback ?? 0,
icon: (
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
</svg>
),
variant: 'default' as const,
},
{
title: t('monitoring.feedback.totalLikes'),
value: stats?.totalLikes ?? 0,
icon: <ThumbsUp className="w-6 h-6" />,
variant: 'success' as const,
},
{
title: t('monitoring.feedback.totalDislikes'),
value: stats?.totalDislikes ?? 0,
icon: <ThumbsDown className="w-6 h-6" />,
variant: 'danger' as const,
},
{
title: t('monitoring.feedback.satisfactionRate'),
value: stats ? `${stats.satisfactionRate}%` : '0%',
icon: (
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z" />
</svg>
),
variant: (stats && stats.satisfactionRate >= 80
? 'success'
: stats && stats.satisfactionRate >= 50
? 'warning'
: 'danger') as 'default' | 'success' | 'warning' | 'danger',
},
];
return (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
{cards.map((card, index) => (
<FeedbackCard
key={index}
title={card.title}
value={card.value}
icon={card.icon}
variant={card.variant}
loading={loading}
/>
))}
</div>
);
}
@@ -0,0 +1,275 @@
'use client';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
ThumbsUp,
ThumbsDown,
ChevronRight,
ChevronDown,
ExternalLink,
} from 'lucide-react';
import { FeedbackRecord } from '../types/monitoring';
import { Button } from '@/components/ui/button';
import { LoadingSpinner } from '@/components/ui/loading-spinner';
interface FeedbackListProps {
feedback: FeedbackRecord[];
loading?: boolean;
onViewMessage?: (messageId: string) => void;
}
export function FeedbackList({
feedback,
loading,
onViewMessage,
}: FeedbackListProps) {
const { t } = useTranslation();
const [expandedId, setExpandedId] = React.useState<string | null>(null);
const toggleExpand = (id: string) => {
setExpandedId(expandedId === id ? null : id);
};
if (loading) {
return (
<div className="py-12 flex justify-center">
<LoadingSpinner text={t('common.loading')} />
</div>
);
}
if (!feedback || feedback.length === 0) {
return (
<div className="text-center text-gray-500 dark:text-gray-400 py-16">
<svg
className="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
<p className="text-base font-medium mb-2">
{t('monitoring.feedback.noFeedback')}
</p>
<p className="text-sm">
{t('monitoring.feedback.noFeedbackDescription')}
</p>
</div>
);
}
return (
<div className="space-y-4">
{feedback.map((item) => (
<div
key={item.id}
className={`border rounded-xl overflow-hidden hover:shadow-md transition-all duration-200 ${
item.feedbackType === 'like'
? 'border-green-200 dark:border-green-900'
: 'border-red-200 dark:border-red-900'
}`}
>
{/* Header */}
<div
className={`p-5 cursor-pointer transition-colors ${
item.feedbackType === 'like'
? 'hover:bg-green-50 dark:hover:bg-green-950/50 bg-green-50/50 dark:bg-green-950/30'
: 'hover:bg-red-50 dark:hover:bg-red-950/50 bg-red-50/50 dark:bg-red-950/30'
}`}
onClick={() => toggleExpand(item.id)}
>
<div className="flex items-start justify-between">
<div className="flex items-start flex-1">
{/* Expand Icon */}
<div className="mr-3 mt-0.5">
{expandedId === item.id ? (
<ChevronDown
className={`w-5 h-5 ${item.feedbackType === 'like' ? 'text-green-500' : 'text-red-500'}`}
/>
) : (
<ChevronRight
className={`w-5 h-5 ${item.feedbackType === 'like' ? 'text-green-500' : 'text-red-500'}`}
/>
)}
</div>
{/* Content */}
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
{/* Feedback Type Icon */}
{item.feedbackType === 'like' ? (
<ThumbsUp className="w-5 h-5 text-green-500" />
) : (
<ThumbsDown className="w-5 h-5 text-red-500" />
)}
<span
className={`text-sm font-medium ${item.feedbackType === 'like' ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}
>
{item.feedbackType === 'like'
? t('monitoring.feedback.like')
: t('monitoring.feedback.dislike')}
</span>
{item.botName && (
<>
<span className="text-gray-400"></span>
<span className="text-sm text-gray-600 dark:text-gray-400">
{item.botName}
</span>
</>
)}
{item.platform && (
<span className="text-xs px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400">
{item.platform}
</span>
)}
</div>
{item.feedbackContent && (
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
{item.feedbackContent}
</p>
)}
{item.inaccurateReasons &&
item.inaccurateReasons.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{item.inaccurateReasons.map((reason, idx) => (
<span
key={idx}
className="text-xs px-2 py-0.5 rounded bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400"
>
{reason}
</span>
))}
</div>
)}
</div>
</div>
{/* Timestamp */}
<div className="flex flex-col items-end gap-2 ml-4">
<span className="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
{item.timestamp.toLocaleString()}
</span>
</div>
</div>
</div>
{/* Expanded Details */}
{expandedId === item.id && (
<div
className={`border-t p-5 bg-white dark:bg-gray-900 ${
item.feedbackType === 'like'
? 'border-green-200 dark:border-green-900'
: 'border-red-200 dark:border-red-900'
}`}
>
<div className="space-y-4 pl-8 border-l-2 border-gray-200 dark:border-gray-700 ml-4">
{/* Context Info */}
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
{t('monitoring.feedback.contextInfo')}
</h4>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 text-xs">
{item.botName && (
<div className="bg-white dark:bg-gray-900 rounded p-2">
<div className="text-gray-500 dark:text-gray-400">
{t('monitoring.messageList.bot')}
</div>
<div className="font-medium text-gray-900 dark:text-white truncate">
{item.botName}
</div>
</div>
)}
{item.pipelineName && (
<div className="bg-white dark:bg-gray-900 rounded p-2">
<div className="text-gray-500 dark:text-gray-400">
{t('monitoring.messageList.pipeline')}
</div>
<div className="font-medium text-gray-900 dark:text-white truncate">
{item.pipelineName}
</div>
</div>
)}
{item.sessionId && (
<div className="bg-white dark:bg-gray-900 rounded p-2">
<div className="text-gray-500 dark:text-gray-400">
{t('monitoring.sessions.sessionId')}
</div>
<div className="font-medium text-gray-900 dark:text-white truncate">
{item.sessionId}
</div>
</div>
)}
{item.userId && (
<div className="bg-white dark:bg-gray-900 rounded p-2">
<div className="text-gray-500 dark:text-gray-400">
{t('monitoring.feedback.userId')}
</div>
<div className="font-medium text-gray-900 dark:text-white truncate">
{item.userId}
</div>
</div>
)}
{item.messageId && (
<div className="bg-white dark:bg-gray-900 rounded p-2">
<div className="text-gray-500 dark:text-gray-400">
{t('monitoring.feedback.messageId')}
</div>
<div className="font-medium text-gray-900 dark:text-white truncate flex items-center gap-1">
<span className="truncate">{item.messageId}</span>
{onViewMessage && (
<Button
variant="ghost"
size="sm"
className="h-5 px-1.5 text-xs shrink-0"
onClick={(e) => {
e.stopPropagation();
onViewMessage(item.messageId!);
}}
>
<ExternalLink className="w-3 h-3" />
</Button>
)}
</div>
</div>
)}
{item.streamId && (
<div className="bg-white dark:bg-gray-900 rounded p-2">
<div className="text-gray-500 dark:text-gray-400">
{t('monitoring.feedback.streamId')}
</div>
<div className="font-medium text-gray-900 dark:text-white truncate">
{item.streamId}
</div>
</div>
)}
</div>
</div>
{/* Feedback Content */}
{item.feedbackContent && (
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
{t('monitoring.feedback.feedbackContent')}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap">
{item.feedbackContent}
</p>
</div>
)}
</div>
</div>
)}
</div>
))}
</div>
);
}
@@ -0,0 +1,192 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { httpClient } from '@/app/infra/http';
import { FeedbackRecord, FeedbackStats } from '../types/monitoring';
interface UseFeedbackDataParams {
botIds?: string[];
pipelineIds?: string[];
startTime?: string;
endTime?: string;
feedbackType?: 'like' | 'dislike';
limit?: number;
offset?: number;
}
interface RawFeedbackRecord {
id: string;
timestamp: string;
feedback_id: string;
feedback_type: number;
feedback_content?: string;
inaccurate_reasons?: string;
bot_id?: string;
bot_name?: string;
pipeline_id?: string;
pipeline_name?: string;
session_id?: string;
message_id?: string;
stream_id?: string;
user_id?: string;
platform?: string;
}
interface RawFeedbackStats {
total_feedback: number;
total_likes: number;
total_dislikes: number;
satisfaction_rate: number;
by_bot?: Array<{
bot_id: string;
bot_name: string;
total: number;
likes: number;
dislikes: number;
}>;
}
/**
* Custom hook for fetching and managing feedback data
*/
export function useFeedbackData(params: UseFeedbackDataParams = {}) {
const [feedback, setFeedback] = useState<FeedbackRecord[]>([]);
const [stats, setStats] = useState<FeedbackStats | null>(null);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const paramsStr = useMemo(() => JSON.stringify(params), [params]);
const fetchStats = useCallback(async () => {
try {
const queryParams = new URLSearchParams();
if (params.botIds) {
params.botIds.forEach((id) => queryParams.append('botId', id));
}
if (params.pipelineIds) {
params.pipelineIds.forEach((id) =>
queryParams.append('pipelineId', id),
);
}
if (params.startTime) {
queryParams.append('startTime', params.startTime);
}
if (params.endTime) {
queryParams.append('endTime', params.endTime);
}
const result = await httpClient.get<RawFeedbackStats>(
`/api/v1/monitoring/feedback/stats?${queryParams.toString()}`,
);
if (result) {
setStats({
totalFeedback: result.total_feedback,
totalLikes: result.total_likes,
totalDislikes: result.total_dislikes,
satisfactionRate: result.satisfaction_rate,
byBot: result.by_bot?.map((bot) => ({
botId: bot.bot_id,
botName: bot.bot_name,
totalFeedback: bot.total,
totalLikes: bot.likes,
totalDislikes: bot.dislikes,
satisfactionRate:
bot.total > 0 ? Math.round((bot.likes / bot.total) * 100) : 0,
})),
});
}
} catch (err) {
console.error('Failed to fetch feedback stats:', err);
}
}, [params.botIds, params.pipelineIds, params.startTime, params.endTime]);
const fetchFeedback = useCallback(async () => {
setLoading(true);
setError(null);
try {
const queryParams = new URLSearchParams();
if (params.botIds) {
params.botIds.forEach((id) => queryParams.append('botId', id));
}
if (params.pipelineIds) {
params.pipelineIds.forEach((id) =>
queryParams.append('pipelineId', id),
);
}
if (params.startTime) {
queryParams.append('startTime', params.startTime);
}
if (params.endTime) {
queryParams.append('endTime', params.endTime);
}
if (params.feedbackType) {
queryParams.append(
'feedbackType',
params.feedbackType === 'like' ? '1' : '2',
);
}
if (params.limit) {
queryParams.append('limit', params.limit.toString());
}
if (params.offset) {
queryParams.append('offset', params.offset.toString());
}
const result = await httpClient.get<{
feedback: RawFeedbackRecord[];
total: number;
}>(`/api/v1/monitoring/feedback?${queryParams.toString()}`);
if (result) {
const transformedFeedback: FeedbackRecord[] = result.feedback.map(
(item) => ({
id: item.id,
timestamp: new Date(item.timestamp),
feedbackId: item.feedback_id,
feedbackType: item.feedback_type === 1 ? 'like' : 'dislike',
feedbackContent: item.feedback_content,
inaccurateReasons: item.inaccurate_reasons
? JSON.parse(item.inaccurate_reasons)
: undefined,
botId: item.bot_id,
botName: item.bot_name,
pipelineId: item.pipeline_id,
pipelineName: item.pipeline_name,
sessionId: item.session_id,
messageId: item.message_id,
streamId: item.stream_id,
userId: item.user_id,
platform: item.platform,
}),
);
setFeedback(transformedFeedback);
setTotal(result.total);
}
} catch (err) {
setError(err as Error);
console.error('Failed to fetch feedback:', err);
} finally {
setLoading(false);
}
}, [params]);
const refetch = useCallback(() => {
fetchStats();
fetchFeedback();
}, [fetchStats, fetchFeedback]);
useEffect(() => {
refetch();
}, [paramsStr]);
return {
feedback,
stats,
total,
loading,
error,
refetch,
};
}
+97 -1
View File
@@ -1,6 +1,6 @@
'use client';
import React, { Suspense, useState } from 'react';
import React, { Suspense, useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
@@ -10,8 +10,11 @@ import MonitoringFilters from './components/filters/MonitoringFilters';
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 { FeedbackStatsCards } from './components/FeedbackCard';
import { FeedbackList } from './components/FeedbackList';
import { MessageDetails } from './types/monitoring';
import { httpClient } from '@/app/infra/http/HttpClient';
import { LoadingSpinner, LoadingPage } from '@/components/ui/loading-spinner';
@@ -68,6 +71,64 @@ function MonitoringPageContent() {
useMonitoringFilters();
const { data, loading, refetch } = useMonitoringData(filterState);
// Get time range for feedback data
const feedbackTimeRange = useMemo(() => {
const now = new Date();
let startTime: Date | null = null;
switch (filterState.timeRange) {
case 'lastHour':
startTime = new Date(now.getTime() - 60 * 60 * 1000);
break;
case 'last6Hours':
startTime = new Date(now.getTime() - 6 * 60 * 60 * 1000);
break;
case 'last24Hours':
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
break;
case 'last7Days':
startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
break;
case 'last30Days':
startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
break;
case 'custom':
if (filterState.customDateRange) {
startTime = filterState.customDateRange.from;
}
break;
}
const endTime =
filterState.timeRange === 'custom' && filterState.customDateRange
? filterState.customDateRange.to
: now;
return {
startTime: startTime?.toISOString(),
endTime: endTime.toISOString(),
};
}, [filterState.timeRange, filterState.customDateRange]);
// Feedback data hook
const {
feedback: feedbackList,
stats: feedbackStats,
loading: feedbackLoading,
} = useFeedbackData({
botIds:
filterState.selectedBots.length > 0
? filterState.selectedBots
: undefined,
pipelineIds:
filterState.selectedPipelines.length > 0
? filterState.selectedPipelines
: undefined,
startTime: feedbackTimeRange.startTime,
endTime: feedbackTimeRange.endTime,
limit: 50,
});
const [expandedMessageId, setExpandedMessageId] = useState<string | null>(
null,
);
@@ -249,6 +310,9 @@ function MonitoringPageContent() {
<TabsTrigger value="modelCalls" className="px-6 py-2">
{t('monitoring.tabs.modelCalls')}
</TabsTrigger>
<TabsTrigger value="feedback" className="px-6 py-2">
{t('monitoring.tabs.feedback')}
</TabsTrigger>
<TabsTrigger value="errors" className="px-6 py-2">
{t('monitoring.tabs.errors')}
</TabsTrigger>
@@ -609,6 +673,38 @@ function MonitoringPageContent() {
</div>
</TabsContent>
<TabsContent value="feedback" className="p-6 m-0">
<div>
{loading && (
<div className="py-12 flex justify-center">
<LoadingSpinner text={t('common.loading')} />
</div>
)}
{!loading && (
<>
{/* Feedback Stats Cards */}
<div className="mb-6">
<FeedbackStatsCards
stats={feedbackStats}
loading={feedbackLoading}
/>
</div>
{/* Feedback List */}
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{t('monitoring.feedback.feedbackList')}
</h3>
<FeedbackList
feedback={feedbackList}
loading={feedbackLoading}
onViewMessage={jumpToMessage}
/>
</>
)}
</div>
</TabsContent>
<TabsContent value="errors" className="p-6 m-0">
<div>
{loading && (
@@ -162,6 +162,39 @@ export interface DateRange {
to: Date;
}
export interface FeedbackRecord {
id: string;
timestamp: Date;
feedbackId: string;
feedbackType: 'like' | 'dislike';
feedbackContent?: string;
inaccurateReasons?: string[];
botId?: string;
botName?: string;
pipelineId?: string;
pipelineName?: string;
sessionId?: string;
messageId?: string;
streamId?: string;
userId?: string;
platform?: string;
}
export interface FeedbackStats {
totalFeedback: number;
totalLikes: number;
totalDislikes: number;
satisfactionRate: number;
byBot?: Array<{
botId: string;
botName: string;
totalFeedback: number;
totalLikes: number;
totalDislikes: number;
satisfactionRate: number;
}>;
}
export interface MonitoringData {
overview: OverviewMetrics;
messages: MonitoringMessage[];
@@ -170,11 +203,14 @@ export interface MonitoringData {
modelCalls: ModelCall[];
sessions: SessionInfo[];
errors: ErrorLog[];
feedback?: FeedbackRecord[];
feedbackStats?: FeedbackStats;
totalCount: {
messages: number;
llmCalls: number;
embeddingCalls: number;
sessions: number;
errors: number;
feedback?: number;
};
}
@@ -0,0 +1,23 @@
/**
* Resolves the documentation URL for a given adapter from its
* spec.help_links map, selecting the best match for the current locale
* with a fallback to English.
*/
export function getAdapterDocUrl(
helpLinks: Record<string, string> | undefined,
locale: string,
): string | null {
if (!helpLinks) return null;
// Map locale to simplified language key
let lang: string;
if (locale.startsWith('zh')) {
lang = 'zh';
} else if (locale.startsWith('ja')) {
lang = 'ja';
} else {
lang = 'en';
}
return helpLinks[lang] ?? helpLinks['en'] ?? null;
}
+1
View File
@@ -118,6 +118,7 @@ export interface Adapter {
icon?: string;
spec: {
categories?: string[];
help_links?: Record<string, string>;
config: IDynamicFormItemSchema[];
};
}
+49 -5
View File
@@ -13,6 +13,7 @@ import {
PartyPopper,
Loader2,
X,
ExternalLink,
} from 'lucide-react';
import { httpClient } from '@/app/infra/http/HttpClient';
@@ -45,6 +46,8 @@ import {
groupByCategory,
getCategoryLabel,
} from '@/app/infra/entities/adapter-categories';
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
import i18n from 'i18next';
import { Button } from '@/components/ui/button';
import {
@@ -798,6 +801,24 @@ function StepPlatform({
<p className="text-sm text-muted-foreground line-clamp-2">
{extractI18nObject(adapter.description)}
</p>
{(() => {
const docUrl = getAdapterDocUrl(
adapter.spec.help_links,
i18n.language,
);
return docUrl ? (
<a
href={docUrl}
target="_blank"
rel="noopener noreferrer"
className="mt-2 inline-flex items-center text-xs text-primary hover:underline"
onClick={(e) => e.stopPropagation()}
>
<ExternalLink className="mr-1 h-3 w-3" />
{t('bots.viewAdapterDocs')}
</a>
) : null;
})()}
</CardContent>
</Card>
))}
@@ -867,11 +888,34 @@ function StepBotConfig({
{adapterConfigItems.length > 0 && (
<Card>
<CardHeader className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
<CardTitle className="text-base">
{t('wizard.config.platformConfig', {
platform: adapterLabel,
})}
</CardTitle>
<div className="flex items-center gap-2">
<CardTitle className="text-base">
{t('wizard.config.platformConfig', {
platform: adapterLabel,
})}
</CardTitle>
{selectedAdapterName &&
(() => {
const selectedAdapter = adapters.find(
(a) => a.name === selectedAdapterName,
);
const docUrl = getAdapterDocUrl(
selectedAdapter?.spec.help_links,
i18n.language,
);
return docUrl ? (
<a
href={docUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center text-xs text-primary hover:underline"
>
<ExternalLink className="mr-1 h-3 w-3" />
{t('bots.viewAdapterDocs')}
</a>
) : null;
})()}
</div>
<Button
size="sm"
onClick={onSaveBot}
+23 -2
View File
@@ -65,8 +65,7 @@ const enUS = {
privacyPolicy: 'Privacy Policy',
and: 'and',
dataCollectionPolicy: 'Data Collection Policy',
dataCollectionPolicyUrl:
'https://docs.langbot.app/en/insight/data-collection-policy',
dataCollectionPolicyUrl: 'https://link.langbot.app/en/docs/data-policy',
loading: 'Loading...',
fieldRequired: 'This field is required',
or: 'or',
@@ -289,6 +288,7 @@ const enUS = {
platformAdapter: 'Platform/Adapter Selection',
selectAdapter: 'Select Adapter',
adapterConfig: 'Adapter Configuration',
viewAdapterDocs: 'View Docs',
bindPipeline: 'Bind Pipeline',
selectPipeline: 'Select Pipeline',
selectBot: 'Select Bot',
@@ -1031,6 +1031,7 @@ const enUS = {
llmCalls: 'LLM Calls',
embeddingCalls: 'Embedding Calls',
modelCalls: 'Model Calls',
feedback: 'User Feedback',
sessions: 'Session Analysis',
errors: 'Error Logs',
},
@@ -1110,6 +1111,26 @@ const enUS = {
noErrors: 'No errors found',
stackTrace: 'Stack Trace',
},
feedback: {
title: 'User Feedback',
totalFeedback: 'Total Feedback',
totalLikes: 'Likes',
totalDislikes: 'Dislikes',
satisfactionRate: 'Satisfaction Rate',
like: 'Like',
dislike: 'Dislike',
noFeedback: 'No feedback yet',
noFeedbackDescription: 'User feedback will appear here',
feedbackList: 'Feedback List',
feedbackContent: 'Feedback Content',
contextInfo: 'Context Info',
userId: 'User ID',
messageId: 'Message ID',
streamId: 'Stream ID',
inaccurateReasons: 'Inaccurate Reasons',
platform: 'Platform',
exportFeedback: 'Export Feedback',
},
queries: {
title: 'Queries',
},
+23 -2
View File
@@ -67,8 +67,7 @@ const esES = {
privacyPolicy: 'Política de privacidad',
and: 'y',
dataCollectionPolicy: 'Política de recopilación de datos',
dataCollectionPolicyUrl:
'https://docs.langbot.app/en/insight/data-collection-policy',
dataCollectionPolicyUrl: 'https://link.langbot.app/en/docs/data-policy',
loading: 'Cargando...',
fieldRequired: 'Este campo es obligatorio',
or: 'o',
@@ -298,6 +297,7 @@ const esES = {
platformAdapter: 'Selección de plataforma/adaptador',
selectAdapter: 'Seleccionar adaptador',
adapterConfig: 'Configuración del adaptador',
viewAdapterDocs: 'Ver documentación',
bindPipeline: 'Vincular Pipeline',
selectPipeline: 'Seleccionar Pipeline',
selectBot: 'Seleccionar Bot',
@@ -1062,6 +1062,7 @@ const esES = {
embeddingCalls: 'Llamadas Embedding',
modelCalls: 'Llamadas a modelos',
sessions: 'Análisis de sesiones',
feedback: 'Comentarios de usuarios',
errors: 'Registros de errores',
},
messageList: {
@@ -1141,6 +1142,26 @@ const esES = {
noErrors: 'No se encontraron errores',
stackTrace: 'Traza de pila',
},
feedback: {
title: 'Comentarios de usuarios',
totalFeedback: 'Total de comentarios',
totalLikes: 'Me gusta',
totalDislikes: 'No me gusta',
satisfactionRate: 'Tasa de satisfacción',
like: 'Me gusta',
dislike: 'No me gusta',
noFeedback: 'Aún no hay comentarios',
noFeedbackDescription: 'Los comentarios de los usuarios aparecerán aquí',
feedbackList: 'Lista de comentarios',
feedbackContent: 'Contenido del comentario',
contextInfo: 'Información de contexto',
userId: 'ID de usuario',
messageId: 'ID de mensaje',
streamId: 'ID de flujo',
inaccurateReasons: 'Razones de inexactitud',
platform: 'Plataforma',
exportFeedback: 'Exportar comentarios',
},
queries: {
title: 'Consultas',
},
+23 -2
View File
@@ -66,8 +66,7 @@
privacyPolicy: 'プライバシーポリシー',
and: 'および',
dataCollectionPolicy: 'データ収集ポリシー',
dataCollectionPolicyUrl:
'https://docs.langbot.app/ja/insight/data-collection-policy',
dataCollectionPolicyUrl: 'https://link.langbot.app/ja/docs/data-policy',
loading: '読み込み中...',
fieldRequired: 'この項目は必須です',
or: 'または',
@@ -294,6 +293,7 @@
platformAdapter: 'プラットフォーム/アダプター選択',
selectAdapter: 'アダプターを選択',
adapterConfig: 'アダプター設定',
viewAdapterDocs: 'ドキュメントを見る',
bindPipeline: 'パイプラインを紐付け',
selectPipeline: 'パイプラインを選択',
selectBot: 'ボットを選択してください',
@@ -1024,6 +1024,7 @@
embeddingCalls: 'Embedding呼び出し',
modelCalls: 'モデル呼び出し',
sessions: 'セッション分析',
feedback: 'ユーザーフィードバック',
errors: 'エラーログ',
},
messageList: {
@@ -1093,6 +1094,26 @@
stackTrace: 'スタックトレース',
title: 'エラー',
},
feedback: {
title: 'ユーザーフィードバック',
totalFeedback: 'フィードバック合計',
totalLikes: 'いいね数',
totalDislikes: 'よくないね数',
satisfactionRate: '満足度',
like: 'いいね',
dislike: 'よくないね',
noFeedback: 'フィードバックはまだありません',
noFeedbackDescription: 'ユーザーフィードバックがここに表示されます',
feedbackList: 'フィードバック一覧',
feedbackContent: 'フィードバック内容',
contextInfo: 'コンテキスト情報',
userId: 'ユーザーID',
messageId: 'メッセージID',
streamId: 'ストリームID',
inaccurateReasons: '不正確な理由',
platform: 'プラットフォーム',
exportFeedback: 'フィードバックをエクスポート',
},
messageDetails: {
noData: 'このクエリにはLLM呼び出しやエラーがありません',
},
+23 -2
View File
@@ -65,8 +65,7 @@ const thTH = {
privacyPolicy: 'นโยบายความเป็นส่วนตัว',
and: 'และ',
dataCollectionPolicy: 'นโยบายการเก็บรวบรวมข้อมูล',
dataCollectionPolicyUrl:
'https://docs.langbot.app/en/insight/data-collection-policy',
dataCollectionPolicyUrl: 'https://link.langbot.app/en/docs/data-policy',
loading: 'กำลังโหลด...',
fieldRequired: 'ช่องนี้จำเป็นต้องกรอก',
or: 'หรือ',
@@ -284,6 +283,7 @@ const thTH = {
platformAdapter: 'การเลือกแพลตฟอร์ม/อะแดปเตอร์',
selectAdapter: 'เลือกอะแดปเตอร์',
adapterConfig: 'การกำหนดค่าอะแดปเตอร์',
viewAdapterDocs: 'ดูเอกสาร',
bindPipeline: 'ผูก Pipeline',
selectPipeline: 'เลือก Pipeline',
selectBot: 'เลือก Bot',
@@ -1011,6 +1011,7 @@ const thTH = {
embeddingCalls: 'การเรียก Embedding',
modelCalls: 'การเรียกโมเดล',
sessions: 'การวิเคราะห์เซสชัน',
feedback: 'ความคิดเห็นผู้ใช้',
errors: 'บันทึกข้อผิดพลาด',
},
messageList: {
@@ -1089,6 +1090,26 @@ const thTH = {
noErrors: 'ไม่พบข้อผิดพลาด',
stackTrace: 'Stack Trace',
},
feedback: {
title: 'ความคิดเห็นผู้ใช้',
totalFeedback: 'ความคิดเห็นทั้งหมด',
totalLikes: 'ถูกใจ',
totalDislikes: 'ไม่ถูกใจ',
satisfactionRate: 'อัตราความพึงพอใจ',
like: 'ถูกใจ',
dislike: 'ไม่ถูกใจ',
noFeedback: 'ยังไม่มีความคิดเห็น',
noFeedbackDescription: 'ความคิดเห็นของผู้ใช้จะแสดงที่นี่',
feedbackList: 'รายการความคิดเห็น',
feedbackContent: 'เนื้อหาความคิดเห็น',
contextInfo: 'ข้อมูลบริบท',
userId: 'ID ผู้ใช้',
messageId: 'ID ข้อความ',
streamId: 'ID สตรีม',
inaccurateReasons: 'เหตุผลที่ไม่ถูกต้อง',
platform: 'แพลตฟอร์ม',
exportFeedback: 'ส่งออกความคิดเห็น',
},
queries: {
title: 'คำค้นหา',
},
+23 -2
View File
@@ -65,8 +65,7 @@ const viVN = {
privacyPolicy: 'Chính sách bảo mật',
and: 'và',
dataCollectionPolicy: 'Chính sách thu thập dữ liệu',
dataCollectionPolicyUrl:
'https://docs.langbot.app/en/insight/data-collection-policy',
dataCollectionPolicyUrl: 'https://link.langbot.app/en/docs/data-policy',
loading: 'Đang tải...',
fieldRequired: 'Trường này là bắt buộc',
or: 'hoặc',
@@ -293,6 +292,7 @@ const viVN = {
platformAdapter: 'Nền tảng/Lựa chọn Adapter',
selectAdapter: 'Chọn Adapter',
adapterConfig: 'Cấu hình Adapter',
viewAdapterDocs: 'Xem tài liệu',
bindPipeline: 'Liên kết Pipeline',
selectPipeline: 'Chọn Pipeline',
selectBot: 'Chọn Bot',
@@ -1032,6 +1032,7 @@ const viVN = {
embeddingCalls: 'Cuộc gọi Embedding',
modelCalls: 'Cuộc gọi mô hình',
sessions: 'Phân tích phiên',
feedback: 'Phản hồi người dùng',
errors: 'Nhật ký lỗi',
},
messageList: {
@@ -1110,6 +1111,26 @@ const viVN = {
noErrors: 'Không tìm thấy lỗi',
stackTrace: 'Stack Trace',
},
feedback: {
title: 'Phản hồi người dùng',
totalFeedback: 'Tổng phản hồi',
totalLikes: 'Lượt thích',
totalDislikes: 'Lượt không thích',
satisfactionRate: 'Tỷ lệ hài lòng',
like: 'Thích',
dislike: 'Không thích',
noFeedback: 'Chưa có phản hồi',
noFeedbackDescription: 'Phản hồi của người dùng sẽ hiển thị tại đây',
feedbackList: 'Danh sách phản hồi',
feedbackContent: 'Nội dung phản hồi',
contextInfo: 'Thông tin ngữ cảnh',
userId: 'ID người dùng',
messageId: 'ID tin nhắn',
streamId: 'ID luồng',
inaccurateReasons: 'Lý do không chính xác',
platform: 'Nền tảng',
exportFeedback: 'Xuất phản hồi',
},
queries: {
title: 'Truy vấn',
},
+23 -2
View File
@@ -64,8 +64,7 @@ const zhHans = {
privacyPolicy: '隐私政策',
and: '和',
dataCollectionPolicy: '数据收集政策',
dataCollectionPolicyUrl:
'https://docs.langbot.app/zh/insight/data-collection-policy',
dataCollectionPolicyUrl: 'https://link.langbot.app/zh/docs/data-policy',
loading: '加载中...',
fieldRequired: '此字段为必填项',
or: '或',
@@ -277,6 +276,7 @@ const zhHans = {
platformAdapter: '平台/适配器选择',
selectAdapter: '选择适配器',
adapterConfig: '适配器配置',
viewAdapterDocs: '查看文档',
bindPipeline: '绑定流水线',
selectPipeline: '选择流水线',
selectBot: '请选择机器人',
@@ -977,6 +977,7 @@ const zhHans = {
llmCalls: 'LLM调用',
embeddingCalls: 'Embedding调用',
modelCalls: '模型调用',
feedback: '用户反馈',
sessions: '会话分析',
errors: '错误日志',
},
@@ -1056,6 +1057,26 @@ const zhHans = {
noErrors: '未找到错误',
stackTrace: '堆栈追踪',
},
feedback: {
title: '用户反馈',
totalFeedback: '总反馈数',
totalLikes: '点赞数',
totalDislikes: '点踩数',
satisfactionRate: '满意度',
like: '点赞',
dislike: '点踩',
noFeedback: '暂无反馈',
noFeedbackDescription: '用户反馈将在此显示',
feedbackList: '反馈列表',
feedbackContent: '反馈内容',
contextInfo: '上下文信息',
userId: '用户ID',
messageId: '消息ID',
streamId: '流ID',
inaccurateReasons: '不准确原因',
platform: '平台',
exportFeedback: '导出反馈',
},
queries: {
title: '查询记录',
},
+23 -2
View File
@@ -64,8 +64,7 @@ const zhHant = {
privacyPolicy: '隱私政策',
and: '和',
dataCollectionPolicy: '數據收集政策',
dataCollectionPolicyUrl:
'https://docs.langbot.app/zh/insight/data-collection-policy',
dataCollectionPolicyUrl: 'https://link.langbot.app/zh/docs/data-policy',
loading: '載入中...',
fieldRequired: '此欄位為必填',
or: '或',
@@ -276,6 +275,7 @@ const zhHant = {
platformAdapter: '平台/適配器選擇',
selectAdapter: '選擇適配器',
adapterConfig: '適配器設定',
viewAdapterDocs: '查看文檔',
bindPipeline: '綁定流程線',
selectPipeline: '選擇流程線',
selectBot: '請選擇機器人',
@@ -963,6 +963,7 @@ const zhHant = {
embeddingCalls: 'Embedding調用',
modelCalls: '模型調用',
sessions: '會話分析',
feedback: '使用者反饋',
errors: '錯誤日誌',
},
messageList: {
@@ -1032,6 +1033,26 @@ const zhHant = {
stackTrace: '堆疊追蹤',
title: '錯誤',
},
feedback: {
title: '使用者反饋',
totalFeedback: '總反饋數',
totalLikes: '按讚數',
totalDislikes: '按倒讚數',
satisfactionRate: '滿意度',
like: '按讚',
dislike: '按倒讚',
noFeedback: '暫無反饋',
noFeedbackDescription: '使用者反饋將在此顯示',
feedbackList: '反饋列表',
feedbackContent: '反饋內容',
contextInfo: '上下文資訊',
userId: '使用者ID',
messageId: '訊息ID',
streamId: '串流ID',
inaccurateReasons: '不準確原因',
platform: '平台',
exportFeedback: '匯出反饋',
},
messageDetails: {
noData: '此查詢沒有LLM調用或錯誤記錄',
},