feat(ui): i18n for model dashboard tables and health check

This commit is contained in:
吹雪 2026-06-11 04:54:44 +09:00
parent 6d04f50e80
commit b00ffa1e04
7 changed files with 177 additions and 48 deletions

View file

@ -8,6 +8,7 @@ import { errorPatterns } from "@/utils/errorPatterns";
import { individualModelHealthCheckCall, latestHealthChecksCall } from "../networking";
import { Table as TableInstance } from "@tanstack/react-table";
import { Team } from "../key_team_helpers/key_list";
import { useTranslation } from "react-i18next";
interface HealthStatus {
status: string;
@ -51,6 +52,7 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
pageSize = 50,
onPageChange,
}) => {
const { t } = useTranslation();
const [modelHealthStatuses, setModelHealthStatuses] = useState<{ [key: string]: HealthStatus }>({});
const [selectedModelsForHealth, setSelectedModelsForHealth] = useState<string[]>([]);
const [allModelsSelected, setAllModelsSelected] = useState<boolean>(false);
@ -547,15 +549,13 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
<div className="mb-6">
<div className="flex justify-between items-center">
<div>
<Title>Model Health Status</Title>
<Text className="text-gray-600 mt-1">
Run health checks on individual models to verify they are working correctly
</Text>
<Title>{t("modelDashboard.healthCheckComponent.title")}</Title>
<Text className="text-gray-600 mt-1">{t("modelDashboard.healthCheckComponent.subtitle")}</Text>
</div>
<div className="flex items-center gap-3">
{selectedModelsForHealth.length > 0 && (
<Button size="sm" variant="light" onClick={() => handleSelectAll(false)} className="px-3 py-1 text-sm">
Clear Selection
{t("modelDashboard.healthCheckComponent.clearSelection")}
</Button>
)}
<Button
@ -566,8 +566,8 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
className="px-3 py-1 text-sm"
>
{selectedModelsForHealth.length > 0 && selectedModelsForHealth.length < all_models_on_proxy.length
? "Run Selected Checks"
: "Run All Checks"}
? t("modelDashboard.healthCheckComponent.runSelectedChecks")
: t("modelDashboard.healthCheckComponent.runAllChecks")}
</Button>
</div>
</div>
@ -578,8 +578,12 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
<div className="flex justify-between items-center mb-3">
<span data-testid="health-results-count" className="text-sm text-gray-700">
{totalCount > 0
? `Showing ${resultsStart} - ${resultsEnd} of ${totalCount} results`
: "Showing 0 results"}
? t("modelDashboard.healthCheckComponent.showingResults", {
start: resultsStart,
end: resultsEnd,
total: totalCount,
})
: t("modelDashboard.healthCheckComponent.showingZeroResults")}
</span>
<div className="flex items-center space-x-2">
@ -590,7 +594,7 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
isLoading || currentPage === 1 ? "bg-gray-100 text-gray-400 cursor-not-allowed" : "hover:bg-gray-50"
}`}
>
Previous
{t("common.previous")}
</button>
<button
onClick={() => handlePageChange(currentPage + 1)}
@ -601,13 +605,14 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
: "hover:bg-gray-50"
}`}
>
Next
{t("common.next")}
</button>
</div>
</div>
)}
<ModelDataTable
columns={healthCheckColumns(
t,
modelHealthStatuses,
selectedModelsForHealth,
allModelsSelected,
@ -628,12 +633,16 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
{/* Error Modal */}
<Modal
title={selectedErrorDetails ? `Health Check Error - ${selectedErrorDetails.modelName}` : "Error Details"}
title={
selectedErrorDetails
? t("modelDashboard.healthCheckComponent.errorModalTitle", { modelName: selectedErrorDetails.modelName })
: t("modelDashboard.healthCheckComponent.errorModalFallbackTitle")
}
open={errorModalVisible}
onCancel={closeErrorModal}
footer={[
<AntdButton key="close" onClick={closeErrorModal}>
Close
{t("common.close")}
</AntdButton>,
]}
width={800}
@ -641,14 +650,14 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
{selectedErrorDetails && (
<div className="space-y-4">
<div>
<Text className="font-medium">Error:</Text>
<Text className="font-medium">{t("modelDashboard.healthCheckComponent.errorLabel")}</Text>
<div className="mt-2 p-3 bg-red-50 border border-red-200 rounded-md">
<Text className="text-red-800">{selectedErrorDetails.cleanedError}</Text>
</div>
</div>
<div>
<Text className="font-medium">Full Error Details:</Text>
<Text className="font-medium">{t("modelDashboard.healthCheckComponent.fullErrorLabel")}</Text>
<div className="mt-2 p-3 bg-gray-50 border border-gray-200 rounded-md max-h-96 overflow-y-auto">
<pre className="text-sm text-gray-800 whitespace-pre-wrap">{selectedErrorDetails.fullError}</pre>
</div>
@ -660,13 +669,17 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
{/* Success Modal */}
<Modal
title={
selectedSuccessDetails ? `Health Check Response - ${selectedSuccessDetails.modelName}` : "Response Details"
selectedSuccessDetails
? t("modelDashboard.healthCheckComponent.successModalTitle", {
modelName: selectedSuccessDetails.modelName,
})
: t("modelDashboard.healthCheckComponent.successModalFallbackTitle")
}
open={successModalVisible}
onCancel={closeSuccessModal}
footer={[
<AntdButton key="close" onClick={closeSuccessModal}>
Close
{t("common.close")}
</AntdButton>,
]}
width={800}
@ -674,14 +687,14 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
{selectedSuccessDetails && (
<div className="space-y-4">
<div>
<Text className="font-medium">Status:</Text>
<Text className="font-medium">{t("modelDashboard.healthCheckComponent.statusLabel")}</Text>
<div className="mt-2 p-3 bg-green-50 border border-green-200 rounded-md">
<Text className="text-green-800">Health check passed successfully</Text>
<Text className="text-green-800">{t("modelDashboard.healthCheckComponent.healthCheckPassed")}</Text>
</div>
</div>
<div>
<Text className="font-medium">Response Details:</Text>
<Text className="font-medium">{t("modelDashboard.healthCheckComponent.responseDetailsLabel")}</Text>
<div className="mt-2 p-3 bg-gray-50 border border-gray-200 rounded-md max-h-96 overflow-y-auto">
<pre className="text-sm text-gray-800 whitespace-pre-wrap">
{JSON.stringify(selectedSuccessDetails.response, null, 2)}

View file

@ -6,6 +6,7 @@ import NotificationsManager from "@/components/molecules/notifications_manager";
import { parseErrorMessage } from "@/components/shared/errorUtils";
import { Button, Form, Modal, Skeleton, Space, Switch, Typography } from "antd";
import React, { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
interface ModelSettingsModalProps {
isVisible: boolean;
@ -14,6 +15,7 @@ interface ModelSettingsModalProps {
}
const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({ isVisible, onCancel, onSuccess }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const { mutateAsync, isPending } = useStoreModelInDB();
const { data: proxyConfigData, isLoading: isLoadingConfig, refetch } = useProxyConfig(ConfigType.GENERAL_SETTINGS);
@ -44,16 +46,20 @@ const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({ isVisible, onCa
try {
await mutateAsync(formValues, {
onSuccess: () => {
NotificationsManager.success("Model storage settings updated successfully");
NotificationsManager.success(t("modelDashboard.modelSettingsModal.updateSuccess"));
refetch();
onSuccess?.();
},
onError: (error) => {
NotificationsManager.fromBackend("Failed to save model storage settings: " + parseErrorMessage(error));
NotificationsManager.fromBackend(
t("modelDashboard.modelSettingsModal.updateFailed", { error: parseErrorMessage(error) }),
);
},
});
} catch (error) {
NotificationsManager.fromBackend("Failed to save model storage settings: " + parseErrorMessage(error));
NotificationsManager.fromBackend(
t("modelDashboard.modelSettingsModal.updateFailed", { error: parseErrorMessage(error) }),
);
}
};
@ -64,15 +70,15 @@ const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({ isVisible, onCa
return (
<Modal
title={<Typography.Title level={5}>Model Settings</Typography.Title>}
title={<Typography.Title level={5}>{t("modelDashboard.modelSettingsModal.title")}</Typography.Title>}
open={isVisible}
footer={
<Space>
<Button onClick={handleCancel} disabled={isPending || isLoadingConfig}>
Cancel
{t("common.cancel")}
</Button>
<Button type="primary" loading={isPending} disabled={isLoadingConfig} onClick={() => form.submit()}>
{isPending ? "Saving..." : "Save Settings"}
{isPending ? t("common.saving") : t("modelDashboard.modelSettingsModal.saveSettings")}
</Button>
</Space>
}
@ -86,11 +92,11 @@ const ModelSettingsModal: React.FC<ModelSettingsModalProps> = ({ isVisible, onCa
initialValues={initialValues}
>
<Form.Item
label="Store Model in DB"
label={t("modelDashboard.modelSettingsModal.storeModelInDb")}
name="store_model_in_db"
tooltip={
proxyConfigData?.find((f) => f.field_name === "store_model_in_db")?.field_description ||
"If enabled, models and config are stored in and loaded from the database."
t("modelDashboard.modelSettingsModal.storeModelInDbTooltip")
}
valuePropName="checked"
>

View file

@ -16,6 +16,7 @@ import {
TableHeaderSortDropdown,
SortState,
} from "../common_components/TableHeaderSortDropdown/TableHeaderSortDropdown";
import { useTranslation } from "react-i18next";
// Extend the column meta type to include className
declare module "@tanstack/react-table" {
@ -47,6 +48,7 @@ export function AllModelsDataTable<TData, TValue>({
enablePagination = false,
onRowClick,
}: AllModelsDataTableProps<TData, TValue>) {
const { t } = useTranslation();
const [columnResizeMode] = React.useState<ColumnResizeMode>("onChange");
const [columnSizing, setColumnSizing] = React.useState({});
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
@ -173,7 +175,7 @@ export function AllModelsDataTable<TData, TValue>({
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>🚅 Loading models...</p>
<p>{t("modelDashboard.allModelsTable.loadingModels")}</p>
</div>
</TableCell>
</TableRow>
@ -207,7 +209,7 @@ export function AllModelsDataTable<TData, TValue>({
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>No models found</p>
<p>{t("modelDashboard.allModelsTable.noModels")}</p>
</div>
</TableCell>
</TableRow>

View file

@ -3,6 +3,7 @@ import { Tooltip, Checkbox } from "antd";
import { Text } from "@tremor/react";
import { InformationCircleIcon, PlayIcon, RefreshIcon } from "@heroicons/react/outline";
import { Team } from "@/components/key_team_helpers/key_list";
import { TFunction } from "i18next";
interface HealthCheckData {
model_name: string;
@ -32,6 +33,7 @@ interface HealthStatus {
}
export const healthCheckColumns = (
t: TFunction,
modelHealthStatuses: { [key: string]: HealthStatus },
selectedModelsForHealth: string[],
allModelsSelected: boolean,
@ -54,7 +56,7 @@ export const healthCheckColumns = (
onChange={(e) => handleSelectAll(e.target.checked)}
onClick={(e) => e.stopPropagation()}
/>
<span>Model ID</span>
<span>{t("modelDashboard.healthCheckColumns.modelId")}</span>
</div>
),
accessorKey: "model_info.id",
@ -85,7 +87,7 @@ export const healthCheckColumns = (
},
},
{
header: "Model Name",
header: () => t("modelDashboard.healthCheckColumns.modelName"),
accessorKey: "model_name",
enableSorting: true,
sortingFn: "alphanumeric",
@ -103,7 +105,7 @@ export const healthCheckColumns = (
},
},
{
header: "Team Alias",
header: () => t("modelDashboard.healthCheckColumns.teamAlias"),
accessorKey: "model_info.team_id",
enableSorting: true,
sortingFn: "alphanumeric",
@ -128,7 +130,7 @@ export const healthCheckColumns = (
},
},
{
header: "Health Status",
header: () => t("modelDashboard.healthCheckColumns.healthStatus"),
accessorKey: "health_status",
enableSorting: true,
sortingFn: (rowA, rowB, columnId) => {
@ -164,7 +166,7 @@ export const healthCheckColumns = (
style={{ animationDelay: "0.4s" }}
></div>
</div>
<Text className="text-gray-600 text-sm">Checking...</Text>
<Text className="text-gray-600 text-sm">{t("modelDashboard.healthCheckColumns.checking")}</Text>
</div>
);
}
@ -177,7 +179,7 @@ export const healthCheckColumns = (
<div className="flex items-center space-x-2">
{getStatusBadge(healthStatus.status)}
{hasSuccessResponse && showSuccessModal && (
<Tooltip title="View response details" placement="top">
<Tooltip title={t("modelDashboard.healthCheckColumns.viewResponseDetails")} placement="top">
<button
onClick={() => showSuccessModal(displayName, modelHealthStatuses[modelId]?.successResponse)}
className="p-1 text-green-600 hover:text-green-800 hover:bg-green-50 rounded cursor-pointer transition-colors"
@ -191,7 +193,7 @@ export const healthCheckColumns = (
},
},
{
header: "Error Details",
header: () => t("modelDashboard.healthCheckColumns.errorDetailsHeader"),
accessorKey: "health_error",
enableSorting: false,
cell: ({ row }) => {
@ -201,7 +203,7 @@ export const healthCheckColumns = (
const healthStatus = modelHealthStatuses[modelId];
if (!healthStatus?.error) {
return <Text className="text-gray-400 text-sm">No errors</Text>;
return <Text className="text-gray-400 text-sm">{t("modelDashboard.healthCheckColumns.noErrors")}</Text>;
}
const cleanedError = healthStatus.error;
@ -215,7 +217,7 @@ export const healthCheckColumns = (
</Tooltip>
</div>
{showErrorModal && fullError !== cleanedError && (
<Tooltip title="View full error details" placement="top">
<Tooltip title={t("modelDashboard.healthCheckColumns.viewFullErrorDetails")} placement="top">
<button
onClick={() => showErrorModal(displayName, cleanedError, fullError)}
className="p-1 text-red-600 hover:text-red-800 hover:bg-red-50 rounded cursor-pointer transition-colors"
@ -229,7 +231,7 @@ export const healthCheckColumns = (
},
},
{
header: "Last Check",
header: () => t("modelDashboard.healthCheckColumns.lastCheck"),
accessorKey: "last_check",
enableSorting: true,
sortingFn: (rowA, rowB, columnId) => {
@ -261,13 +263,13 @@ export const healthCheckColumns = (
return (
<Text className="text-gray-600 text-sm">
{model.health_loading ? "Check in progress..." : model.last_check}
{model.health_loading ? t("modelDashboard.healthCheckColumns.checkInProgress") : model.last_check}
</Text>
);
},
},
{
header: "Last Success",
header: () => t("modelDashboard.healthCheckColumns.lastSuccess"),
accessorKey: "last_success",
enableSorting: true,
sortingFn: (rowA, rowB, columnId) => {
@ -304,7 +306,7 @@ export const healthCheckColumns = (
},
},
{
header: "Actions",
header: () => t("common.actions"),
id: "actions",
cell: ({ row }) => {
const model = row.original;
@ -312,10 +314,10 @@ export const healthCheckColumns = (
const hasExistingStatus = model.health_status && model.health_status !== "none";
const tooltipText = model.health_loading
? "Checking..."
? t("modelDashboard.healthCheckColumns.checking")
: hasExistingStatus
? "Re-run Health Check"
: "Run Health Check";
? t("modelDashboard.healthCheckColumns.rerunHealthCheck")
: t("modelDashboard.healthCheckColumns.runHealthCheck");
return (
<Tooltip title={tooltipText} placement="top">

View file

@ -14,6 +14,7 @@ import {
import React from "react";
import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react";
import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline";
import { useTranslation } from "react-i18next";
// Extend the column meta type to include className
declare module "@tanstack/react-table" {
@ -43,6 +44,7 @@ export function ModelDataTable<TData, TValue>({
enablePagination = false,
onRowClick,
}: ModelDataTableProps<TData, TValue>) {
const { t } = useTranslation();
const [sorting, setSorting] = React.useState<SortingState>(defaultSorting);
const [columnResizeMode] = React.useState<ColumnResizeMode>("onChange");
const [columnSizing, setColumnSizing] = React.useState({});
@ -160,7 +162,7 @@ export function ModelDataTable<TData, TValue>({
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>🚅 Loading models...</p>
<p>{t("modelDashboard.table.loadingModels")}</p>
</div>
</TableCell>
</TableRow>
@ -194,7 +196,7 @@ export function ModelDataTable<TData, TValue>({
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>No models found</p>
<p>{t("modelDashboard.table.noModels")}</p>
</div>
</TableCell>
</TableRow>

View file

@ -7253,5 +7253,57 @@
"updateSuccess": "Search tool updated successfully",
"updateFailed": "Failed to update search tool"
}
},
"modelDashboard": {
"table": {
"loadingModels": "🚅 Loading models...",
"noModels": "No models found"
},
"allModelsTable": {
"loadingModels": "🚅 Loading models...",
"noModels": "No models found"
},
"healthCheckComponent": {
"title": "Model Health Status",
"subtitle": "Run health checks on individual models to verify they are working correctly",
"clearSelection": "Clear Selection",
"runSelectedChecks": "Run Selected Checks",
"runAllChecks": "Run All Checks",
"showingResults": "Showing {{start}} - {{end}} of {{total}} results",
"showingZeroResults": "Showing 0 results",
"errorModalTitle": "Health Check Error - {{modelName}}",
"errorModalFallbackTitle": "Error Details",
"errorLabel": "Error:",
"fullErrorLabel": "Full Error Details:",
"successModalTitle": "Health Check Response - {{modelName}}",
"successModalFallbackTitle": "Response Details",
"statusLabel": "Status:",
"healthCheckPassed": "Health check passed successfully",
"responseDetailsLabel": "Response Details:"
},
"healthCheckColumns": {
"modelId": "Model ID",
"modelName": "Model Name",
"teamAlias": "Team Alias",
"healthStatus": "Health Status",
"checking": "Checking...",
"viewResponseDetails": "View response details",
"errorDetailsHeader": "Error Details",
"noErrors": "No errors",
"viewFullErrorDetails": "View full error details",
"lastCheck": "Last Check",
"checkInProgress": "Check in progress...",
"lastSuccess": "Last Success",
"rerunHealthCheck": "Re-run Health Check",
"runHealthCheck": "Run Health Check"
},
"modelSettingsModal": {
"title": "Model Settings",
"saveSettings": "Save Settings",
"storeModelInDb": "Store Model in DB",
"storeModelInDbTooltip": "If enabled, models and config are stored in and loaded from the database.",
"updateSuccess": "Model storage settings updated successfully",
"updateFailed": "Failed to save model storage settings: {{error}}"
}
}
}

View file

@ -7253,5 +7253,57 @@
"updateSuccess": "搜索工具更新成功",
"updateFailed": "更新搜索工具失败"
}
},
"modelDashboard": {
"table": {
"loadingModels": "🚅 正在加载模型...",
"noModels": "未找到模型"
},
"allModelsTable": {
"loadingModels": "🚅 正在加载模型...",
"noModels": "未找到模型"
},
"healthCheckComponent": {
"title": "模型健康检查状态",
"subtitle": "对各模型运行健康检查,验证其是否正常工作",
"clearSelection": "清除选择",
"runSelectedChecks": "运行所选检查",
"runAllChecks": "运行所有检查",
"showingResults": "显示第 {{start}} - {{end}} 条,共 {{total}} 条结果",
"showingZeroResults": "显示 0 条结果",
"errorModalTitle": "健康检查错误 - {{modelName}}",
"errorModalFallbackTitle": "错误详情",
"errorLabel": "错误:",
"fullErrorLabel": "完整错误详情:",
"successModalTitle": "健康检查响应 - {{modelName}}",
"successModalFallbackTitle": "响应详情",
"statusLabel": "状态:",
"healthCheckPassed": "健康检查通过",
"responseDetailsLabel": "响应详情:"
},
"healthCheckColumns": {
"modelId": "模型 ID",
"modelName": "模型名称",
"teamAlias": "团队别名",
"healthStatus": "健康状态",
"checking": "检查中...",
"viewResponseDetails": "查看响应详情",
"errorDetailsHeader": "错误详情",
"noErrors": "无错误",
"viewFullErrorDetails": "查看完整错误详情",
"lastCheck": "最近检查",
"checkInProgress": "检查进行中...",
"lastSuccess": "最近成功",
"rerunHealthCheck": "重新运行健康检查",
"runHealthCheck": "运行健康检查"
},
"modelSettingsModal": {
"title": "模型设置",
"saveSettings": "保存设置",
"storeModelInDb": "将模型存储到数据库",
"storeModelInDbTooltip": "启用后,模型和配置将存储到数据库并从数据库加载。",
"updateSuccess": "模型存储设置已成功更新",
"updateFailed": "保存模型存储设置失败:{{error}}"
}
}
}