mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for common_components batch 1
This commit is contained in:
parent
019dfcbe23
commit
77fbc36e94
12 changed files with 310 additions and 86 deletions
|
|
@ -2,6 +2,7 @@ import React from "react";
|
|||
import { Select, Skeleton } from "antd";
|
||||
import { TeamOutlined } from "@ant-design/icons";
|
||||
import { Text } from "@tremor/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAccessGroups, AccessGroupResponse } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroups";
|
||||
|
||||
export interface AccessGroupSelectorProps {
|
||||
|
|
@ -28,14 +29,15 @@ export interface AccessGroupSelectorProps {
|
|||
const AccessGroupSelector: React.FC<AccessGroupSelectorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "Select access groups",
|
||||
placeholder,
|
||||
disabled = false,
|
||||
style,
|
||||
className,
|
||||
showLabel = false,
|
||||
labelText = "Access Group",
|
||||
labelText,
|
||||
allowClear = true,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { data: accessGroups, isLoading, isError } = useAccessGroups();
|
||||
|
||||
// ── Loading skeleton ─────────────────────────────────────────────────────
|
||||
|
|
@ -44,7 +46,7 @@ const AccessGroupSelector: React.FC<AccessGroupSelectorProps> = ({
|
|||
<div>
|
||||
{showLabel && (
|
||||
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
|
||||
<TeamOutlined className="mr-2" /> {labelText}
|
||||
<TeamOutlined className="mr-2" /> {labelText ?? t("commonComponents.accessGroupSelector.labelText")}
|
||||
</Text>
|
||||
)}
|
||||
<Skeleton.Input active block style={{ height: 32, ...style }} />
|
||||
|
|
@ -70,13 +72,13 @@ const AccessGroupSelector: React.FC<AccessGroupSelectorProps> = ({
|
|||
<div>
|
||||
{showLabel && (
|
||||
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
|
||||
<TeamOutlined className="mr-2" /> {labelText}
|
||||
<TeamOutlined className="mr-2" /> {labelText ?? t("commonComponents.accessGroupSelector.labelText")}
|
||||
</Text>
|
||||
)}
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
placeholder={placeholder ?? t("commonComponents.accessGroupSelector.placeholder")}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
allowClear={allowClear}
|
||||
|
|
@ -84,7 +86,11 @@ const AccessGroupSelector: React.FC<AccessGroupSelectorProps> = ({
|
|||
style={{ width: "100%", ...style }}
|
||||
className={`rounded-md ${className ?? ""}`}
|
||||
notFoundContent={
|
||||
isError ? <span className="text-red-500">Failed to load access groups</span> : "No access groups found"
|
||||
isError ? (
|
||||
<span className="text-red-500">{t("commonComponents.accessGroupSelector.loadFailed")}</span>
|
||||
) : (
|
||||
t("commonComponents.accessGroupSelector.notFound")
|
||||
)
|
||||
}
|
||||
filterOption={(input, option) => {
|
||||
const searchText = options.find((opt) => opt.value === option?.value)?.searchText ?? "";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from "react";
|
||||
import { Text, Badge } from "@tremor/react";
|
||||
import { RefreshIcon, ClockIcon } from "@heroicons/react/outline";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface AutoRotationViewProps {
|
||||
autoRotate?: boolean;
|
||||
|
|
@ -21,6 +22,8 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
|
|||
variant = "card",
|
||||
className = "",
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formatTimestamp = (timestamp: string | Date) => {
|
||||
const date = new Date(timestamp);
|
||||
const dateStr = date.toLocaleDateString("en-US", {
|
||||
|
|
@ -42,14 +45,16 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
|
|||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<RefreshIcon className="h-4 w-4 text-blue-600" />
|
||||
<Text className="font-semibold text-gray-900">Auto-Rotation</Text>
|
||||
<Text className="font-semibold text-gray-900">{t("commonComponents.autoRotationView.title")}</Text>
|
||||
<Badge color={autoRotate ? "green" : "gray"} size="xs">
|
||||
{autoRotate ? "Enabled" : "Disabled"}
|
||||
{autoRotate ? t("common.enabled") : t("common.disabled")}
|
||||
</Badge>
|
||||
{autoRotate && rotationInterval && (
|
||||
<>
|
||||
<Text className="text-gray-400">•</Text>
|
||||
<Text className="text-sm text-gray-600">Every {rotationInterval}</Text>
|
||||
<Text className="text-sm text-gray-600">
|
||||
{t("commonComponents.autoRotationView.every", { interval: rotationInterval })}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -63,7 +68,7 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
|
|||
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-200 rounded-md">
|
||||
<ClockIcon className="w-4 h-4 text-gray-500" />
|
||||
<div className="flex-1">
|
||||
<Text className="font-medium text-gray-700">Last Rotation</Text>
|
||||
<Text className="font-medium text-gray-700">{t("commonComponents.autoRotationView.lastRotation")}</Text>
|
||||
<Text className="text-sm text-gray-600">{formatTimestamp(lastRotationAt)}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -74,7 +79,9 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
|
|||
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-200 rounded-md">
|
||||
<ClockIcon className="w-4 h-4 text-gray-500" />
|
||||
<div className="flex-1">
|
||||
<Text className="font-medium text-gray-700">Next Scheduled Rotation</Text>
|
||||
<Text className="font-medium text-gray-700">
|
||||
{t("commonComponents.autoRotationView.nextScheduledRotation")}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">{formatTimestamp(nextRotationAt || keyRotationAt || "")}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -84,7 +91,7 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
|
|||
{autoRotate && !lastRotationAt && !keyRotationAt && !nextRotationAt && (
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-100 rounded-md">
|
||||
<ClockIcon className="w-4 h-4 text-gray-500" />
|
||||
<Text className="text-gray-600">No rotation history available</Text>
|
||||
<Text className="text-gray-600">{t("commonComponents.autoRotationView.noRotationHistory")}</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -94,7 +101,7 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
|
|||
{!autoRotate && !lastRotationAt && !keyRotationAt && !nextRotationAt && (
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-100 rounded-md">
|
||||
<RefreshIcon className="w-4 h-4 text-gray-400" />
|
||||
<Text className="text-gray-600">Auto-rotation is not enabled for this key</Text>
|
||||
<Text className="text-gray-600">{t("commonComponents.autoRotationView.notEnabled")}</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -105,8 +112,8 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
|
|||
<div className={`bg-white border border-gray-200 rounded-lg p-6 ${className}`}>
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<div>
|
||||
<Text className="font-semibold text-gray-900">Auto-Rotation</Text>
|
||||
<Text className="text-xs text-gray-500">Automatic key rotation settings and status for this key</Text>
|
||||
<Text className="font-semibold text-gray-900">{t("commonComponents.autoRotationView.title")}</Text>
|
||||
<Text className="text-xs text-gray-500">{t("commonComponents.autoRotationView.subtitle")}</Text>
|
||||
</div>
|
||||
</div>
|
||||
{content}
|
||||
|
|
@ -116,7 +123,7 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
|
|||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<Text className="font-medium text-gray-900 mb-3">Auto-Rotation</Text>
|
||||
<Text className="font-medium text-gray-900 mb-3">{t("commonComponents.autoRotationView.title")}</Text>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Tag, Typography } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
|
|
@ -14,8 +15,10 @@ interface DefaultProxyAdminTagProps {
|
|||
* plain text.
|
||||
*/
|
||||
export default function DefaultProxyAdminTag({ userId }: DefaultProxyAdminTagProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (userId === DEFAULT_USER_ID) {
|
||||
return <Tag color="blue">Default Proxy Admin</Tag>;
|
||||
return <Tag color="blue">{t("commonComponents.defaultProxyAdminTag.label")}</Tag>;
|
||||
}
|
||||
|
||||
return <Text>{userId}</Text>;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Alert, Card, Descriptions, Input, Modal, Typography, theme } from "antd";
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
interface DeleteResourceModalProps {
|
||||
isOpen: boolean;
|
||||
|
|
@ -32,6 +33,7 @@ export default function DeleteResourceModal({
|
|||
confirmLoading,
|
||||
requiredConfirmation,
|
||||
}: DeleteResourceModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { Title, Text } = Typography;
|
||||
const { token } = theme.useToken();
|
||||
const [requiredConfirmationInput, setRequiredConfirmationInput] = useState("");
|
||||
|
|
@ -49,8 +51,8 @@ export default function DeleteResourceModal({
|
|||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
confirmLoading={confirmLoading}
|
||||
okText={confirmLoading ? "Deleting..." : "Delete"}
|
||||
cancelText="Cancel"
|
||||
okText={confirmLoading ? t("common.deleting") : t("commonComponents.deleteResourceModal.okText")}
|
||||
cancelText={t("common.cancel")}
|
||||
okButtonProps={{
|
||||
danger: true,
|
||||
disabled: (!!requiredConfirmation && requiredConfirmationInput !== requiredConfirmation) || confirmLoading,
|
||||
|
|
@ -89,11 +91,13 @@ export default function DeleteResourceModal({
|
|||
{requiredConfirmation && (
|
||||
<div className="mb-6 mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<Text className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<Text>Type </Text>
|
||||
<Text strong type="danger">
|
||||
{requiredConfirmation}
|
||||
</Text>
|
||||
<Text> to confirm deletion:</Text>
|
||||
<Trans
|
||||
i18nKey="commonComponents.deleteResourceModal.typeToConfirm"
|
||||
values={{ name: requiredConfirmation }}
|
||||
components={{
|
||||
danger: <Text strong type="danger" />,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
<Input
|
||||
value={requiredConfirmationInput}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Select } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface DurationSelectProps {
|
||||
className?: string;
|
||||
|
|
@ -7,11 +8,13 @@ interface DurationSelectProps {
|
|||
}
|
||||
|
||||
export default function DurationSelect({ className, value, onChange }: DurationSelectProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Select className={className} value={value} onChange={onChange}>
|
||||
<Select.Option value="24h">Daily</Select.Option>
|
||||
<Select.Option value="7d">Weekly</Select.Option>
|
||||
<Select.Option value="30d">Monthly</Select.Option>
|
||||
<Select.Option value="24h">{t("commonComponents.durationSelect.daily")}</Select.Option>
|
||||
<Select.Option value="7d">{t("commonComponents.durationSelect.weekly")}</Select.Option>
|
||||
<Select.Option value="30d">{t("commonComponents.durationSelect.monthly")}</Select.Option>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState } from "react";
|
|||
import { Select, Tooltip, Divider, Switch, Checkbox } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { TextInput } from "@tremor/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
|
|
@ -26,6 +27,8 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
|
|||
neverExpire = false,
|
||||
onNeverExpireChange,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Predefined intervals
|
||||
const predefinedIntervals = ["7d", "30d", "90d", "180d", "365d"];
|
||||
|
||||
|
|
@ -65,12 +68,14 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
|
|||
<div className="space-y-6">
|
||||
{/* Key Expiry Section */}
|
||||
<div className="space-y-4">
|
||||
<span className="text-sm font-medium text-gray-700">Key Expiry Settings</span>
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{t("commonComponents.keyLifecycleSettings.keyExpirySectionTitle")}
|
||||
</span>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 flex items-center space-x-1">
|
||||
<span>Expire Key</span>
|
||||
<Tooltip title="Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Leave empty to keep the current expiry unchanged.">
|
||||
<span>{t("commonComponents.keyLifecycleSettings.expireKeyLabel")}</span>
|
||||
<Tooltip title={t("commonComponents.keyLifecycleSettings.expireKeyTooltip")}>
|
||||
<InfoCircleOutlined className="text-gray-400 cursor-help text-xs" />
|
||||
</Tooltip>
|
||||
{!isCreateMode && onNeverExpireChange && (
|
||||
|
|
@ -90,13 +95,17 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
|
|||
}}
|
||||
className="ml-2 text-sm font-normal text-gray-600"
|
||||
>
|
||||
Never Expire
|
||||
{t("commonComponents.keyLifecycleSettings.neverExpire")}
|
||||
</Checkbox>
|
||||
)}
|
||||
</label>
|
||||
<TextInput
|
||||
name="duration"
|
||||
placeholder={isCreateMode ? "e.g., 30d or leave empty to never expire" : "e.g., 30d"}
|
||||
placeholder={
|
||||
isCreateMode
|
||||
? t("commonComponents.keyLifecycleSettings.expireKeyPlaceholderCreate")
|
||||
: t("commonComponents.keyLifecycleSettings.expireKeyPlaceholderEdit")
|
||||
}
|
||||
className="w-full"
|
||||
value={durationValue}
|
||||
onValueChange={handleDurationChange}
|
||||
|
|
@ -109,13 +118,15 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
|
|||
|
||||
{/* Auto-Rotation Section */}
|
||||
<div className="space-y-4">
|
||||
<span className="text-sm font-medium text-gray-700">Auto-Rotation Settings</span>
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{t("commonComponents.keyLifecycleSettings.autoRotationSectionTitle")}
|
||||
</span>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 flex items-center space-x-1">
|
||||
<span>Enable Auto-Rotation</span>
|
||||
<Tooltip title="Key will automatically regenerate at the specified interval for enhanced security.">
|
||||
<span>{t("commonComponents.keyLifecycleSettings.enableAutoRotationLabel")}</span>
|
||||
<Tooltip title={t("commonComponents.keyLifecycleSettings.enableAutoRotationTooltip")}>
|
||||
<InfoCircleOutlined className="text-gray-400 cursor-help text-xs" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
|
|
@ -130,8 +141,8 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
|
|||
{autoRotationEnabled && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 flex items-center space-x-1">
|
||||
<span>Rotation Interval</span>
|
||||
<Tooltip title="How often the key should be automatically rotated. Choose the interval that best fits your security requirements.">
|
||||
<span>{t("commonComponents.keyLifecycleSettings.rotationIntervalLabel")}</span>
|
||||
<Tooltip title={t("commonComponents.keyLifecycleSettings.rotationIntervalTooltip")}>
|
||||
<InfoCircleOutlined className="text-gray-400 cursor-help text-xs" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
|
|
@ -140,14 +151,14 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
|
|||
value={showCustomInput ? "custom" : rotationInterval}
|
||||
onChange={handleIntervalChange}
|
||||
className="w-full"
|
||||
placeholder="Select interval"
|
||||
placeholder={t("commonComponents.keyLifecycleSettings.selectInterval")}
|
||||
>
|
||||
<Option value="7d">7 days</Option>
|
||||
<Option value="30d">30 days</Option>
|
||||
<Option value="90d">90 days</Option>
|
||||
<Option value="180d">180 days</Option>
|
||||
<Option value="365d">365 days</Option>
|
||||
<Option value="custom">Custom interval</Option>
|
||||
<Option value="7d">{t("commonComponents.keyLifecycleSettings.sevenDays")}</Option>
|
||||
<Option value="30d">{t("commonComponents.keyLifecycleSettings.thirtyDays")}</Option>
|
||||
<Option value="90d">{t("commonComponents.keyLifecycleSettings.ninetyDays")}</Option>
|
||||
<Option value="180d">{t("commonComponents.keyLifecycleSettings.oneHundredEightyDays")}</Option>
|
||||
<Option value="365d">{t("commonComponents.keyLifecycleSettings.threeHundredSixtyFiveDays")}</Option>
|
||||
<Option value="custom">{t("commonComponents.keyLifecycleSettings.customInterval")}</Option>
|
||||
</Select>
|
||||
|
||||
{showCustomInput && (
|
||||
|
|
@ -155,10 +166,10 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
|
|||
<TextInput
|
||||
value={customInterval}
|
||||
onChange={handleCustomIntervalChange}
|
||||
placeholder="e.g., 1s, 5m, 2h, 14d"
|
||||
placeholder={t("commonComponents.keyLifecycleSettings.customIntervalPlaceholder")}
|
||||
/>
|
||||
<div className="text-xs text-gray-500">
|
||||
Supported formats: seconds (s), minutes (m), hours (h), days (d)
|
||||
{t("commonComponents.keyLifecycleSettings.customIntervalFormats")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -169,8 +180,7 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
|
|||
|
||||
{autoRotationEnabled && (
|
||||
<div className="bg-blue-50 p-3 rounded-md text-sm text-blue-700">
|
||||
When rotation occurs, you'll receive a notification with the new key. The old key will be deactivated
|
||||
after a brief grace period.
|
||||
{t("commonComponents.keyLifecycleSettings.rotationNotice")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import { cx } from "@/lib/cva.config";
|
||||
import { UiLoadingSpinner } from "../ui/ui-loading-spinner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function LoadingScreen() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={cx("h-screen", "flex items-center justify-center gap-4")}>
|
||||
<div className="text-lg font-medium py-2 pr-4 border-r border-r-gray-200">🚅 LiteLLM</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<UiLoadingSpinner className="size-4" />
|
||||
<span className="text-gray-600 text-sm">Loading...</span>
|
||||
<span className="text-gray-600 text-sm">{t("common.loading")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { CrownOutlined, InfoCircleOutlined, UserAddOutlined, UserOutlined } from
|
|||
import { Button, Space, Table, Tag, Tooltip, Typography } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import TableIconActionButton from "./IconActionButton/TableIconActionButtons/TableIconActionButton";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
|
@ -26,36 +27,41 @@ export default function MemberTable({
|
|||
onEdit,
|
||||
onDelete,
|
||||
onAddMember,
|
||||
roleColumnTitle = "Role",
|
||||
roleColumnTitle,
|
||||
roleTooltip,
|
||||
extraColumns = [],
|
||||
showDeleteForMember,
|
||||
emptyText,
|
||||
}: MemberTableProps) {
|
||||
const { t } = useTranslation();
|
||||
const baseColumns: ColumnsType<Member> = [
|
||||
{
|
||||
title: "User Email",
|
||||
title: t("commonComponents.memberTable.userEmail"),
|
||||
dataIndex: "user_email",
|
||||
key: "user_email",
|
||||
render: (email: string | null) => <Text>{email || "-"}</Text>,
|
||||
},
|
||||
{
|
||||
title: "User ID",
|
||||
title: t("commonComponents.memberTable.userId"),
|
||||
dataIndex: "user_id",
|
||||
key: "user_id",
|
||||
render: (userId: string | null) =>
|
||||
userId === "default_user_id" ? <Tag color="blue">Default Proxy Admin</Tag> : <Text>{userId || "-"}</Text>,
|
||||
userId === "default_user_id" ? (
|
||||
<Tag color="blue">{t("commonComponents.memberTable.defaultProxyAdmin")}</Tag>
|
||||
) : (
|
||||
<Text>{userId || "-"}</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: roleTooltip ? (
|
||||
<Space direction="horizontal">
|
||||
{roleColumnTitle}
|
||||
{roleColumnTitle ?? t("user.role")}
|
||||
<Tooltip title={roleTooltip}>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
) : (
|
||||
roleColumnTitle
|
||||
roleColumnTitle ?? t("user.role")
|
||||
),
|
||||
dataIndex: "role",
|
||||
key: "role",
|
||||
|
|
@ -72,7 +78,7 @@ export default function MemberTable({
|
|||
},
|
||||
...extraColumns,
|
||||
{
|
||||
title: "Actions",
|
||||
title: t("common.actions"),
|
||||
key: "actions",
|
||||
fixed: "right" as const,
|
||||
width: 120,
|
||||
|
|
@ -81,14 +87,14 @@ export default function MemberTable({
|
|||
<Space>
|
||||
<TableIconActionButton
|
||||
variant="Edit"
|
||||
tooltipText="Edit member"
|
||||
tooltipText={t("commonComponents.memberTable.editMember")}
|
||||
dataTestId="edit-member"
|
||||
onClick={() => onEdit(record)}
|
||||
/>
|
||||
{(!showDeleteForMember || showDeleteForMember(record)) && (
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
tooltipText="Delete member"
|
||||
tooltipText={t("commonComponents.memberTable.deleteMember")}
|
||||
dataTestId="delete-member"
|
||||
onClick={() => onDelete(record)}
|
||||
/>
|
||||
|
|
@ -101,7 +107,7 @@ export default function MemberTable({
|
|||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<span className="inline-flex text-sm text-gray-700">
|
||||
{members.length} Member{members.length !== 1 ? "s" : ""}
|
||||
{t("commonComponents.memberTable.memberCount", { count: members.length })}
|
||||
</span>
|
||||
<Table
|
||||
columns={baseColumns}
|
||||
|
|
@ -114,7 +120,7 @@ export default function MemberTable({
|
|||
/>
|
||||
{onAddMember && canEdit && (
|
||||
<Button icon={<UserAddOutlined />} type="primary" onClick={onAddMember}>
|
||||
Add Member
|
||||
{t("commonComponents.memberTable.addMember")}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { PlusCircleIcon, PencilIcon, TrashIcon } from "@heroicons/react/outline";
|
||||
import { Card, Title, Text, Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ModelSelector from "./ModelSelector";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
|
||||
|
|
@ -23,6 +24,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
onAliasUpdate,
|
||||
showExampleConfig = true,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [aliases, setAliases] = useState<AliasItem[]>([]);
|
||||
const [newAlias, setNewAlias] = useState({ aliasName: "", targetModel: "" });
|
||||
const [editingAlias, setEditingAlias] = useState<AliasItem | null>(null);
|
||||
|
|
@ -39,13 +41,13 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
|
||||
const handleAddAlias = () => {
|
||||
if (!newAlias.aliasName || !newAlias.targetModel) {
|
||||
NotificationsManager.fromBackend("Please provide both alias name and target model");
|
||||
NotificationsManager.fromBackend(t("commonComponents.modelAliasManager.bothRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicate alias names
|
||||
if (aliases.some((alias) => alias.aliasName === newAlias.aliasName)) {
|
||||
NotificationsManager.fromBackend("An alias with this name already exists");
|
||||
NotificationsManager.fromBackend(t("commonComponents.modelAliasManager.duplicateAlias"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +71,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
onAliasUpdate(aliasObject);
|
||||
}
|
||||
|
||||
NotificationsManager.success("Alias added successfully");
|
||||
NotificationsManager.success(t("commonComponents.modelAliasManager.addSuccess"));
|
||||
};
|
||||
|
||||
const handleEditAlias = (alias: AliasItem) => {
|
||||
|
|
@ -80,13 +82,13 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
if (!editingAlias) return;
|
||||
|
||||
if (!editingAlias.aliasName || !editingAlias.targetModel) {
|
||||
NotificationsManager.fromBackend("Please provide both alias name and target model");
|
||||
NotificationsManager.fromBackend(t("commonComponents.modelAliasManager.bothRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicate alias names (excluding current alias)
|
||||
if (aliases.some((alias) => alias.id !== editingAlias.id && alias.aliasName === editingAlias.aliasName)) {
|
||||
NotificationsManager.fromBackend("An alias with this name already exists");
|
||||
NotificationsManager.fromBackend(t("commonComponents.modelAliasManager.duplicateAlias"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +107,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
onAliasUpdate(aliasObject);
|
||||
}
|
||||
|
||||
NotificationsManager.success("Alias updated successfully");
|
||||
NotificationsManager.success(t("commonComponents.modelAliasManager.updateSuccess"));
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
|
|
@ -126,7 +128,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
onAliasUpdate(aliasObject);
|
||||
}
|
||||
|
||||
NotificationsManager.success("Alias deleted successfully");
|
||||
NotificationsManager.success(t("commonComponents.modelAliasManager.deleteSuccess"));
|
||||
};
|
||||
|
||||
// Convert current aliases to object for config example
|
||||
|
|
@ -141,10 +143,14 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
return (
|
||||
<div className="mt-4">
|
||||
<div className="mb-6">
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2">Add New Alias</Text>
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2">
|
||||
{t("commonComponents.modelAliasManager.addNewAlias")}
|
||||
</Text>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">Alias Name</label>
|
||||
<label className="block text-xs text-gray-500 mb-1">
|
||||
{t("commonComponents.modelAliasManager.aliasName")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newAlias.aliasName}
|
||||
|
|
@ -159,7 +165,9 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">Target Model</label>
|
||||
<label className="block text-xs text-gray-500 mb-1">
|
||||
{t("commonComponents.modelAliasManager.targetModel")}
|
||||
</label>
|
||||
<ModelSelector
|
||||
accessToken={accessToken}
|
||||
value={newAlias.targetModel}
|
||||
|
|
@ -180,21 +188,27 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
className={`flex items-center px-4 py-2 rounded-md text-sm ${!newAlias.aliasName || !newAlias.targetModel ? "bg-gray-300 text-gray-500 cursor-not-allowed" : "bg-green-600 text-white hover:bg-green-700"}`}
|
||||
>
|
||||
<PlusCircleIcon className="w-4 h-4 mr-1" />
|
||||
Add Alias
|
||||
{t("commonComponents.modelAliasManager.addAlias")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2">Manage Existing Aliases</Text>
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2">
|
||||
{t("commonComponents.modelAliasManager.manageExisting")}
|
||||
</Text>
|
||||
<div className="rounded-lg custom-border relative mb-6">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="[&_td]:py-0.5 [&_th]:py-1">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell className="py-1 h-8">Alias Name</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">Target Model</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">Actions</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">
|
||||
{t("commonComponents.modelAliasManager.aliasName")}
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">
|
||||
{t("commonComponents.modelAliasManager.targetModel")}
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">{t("common.actions")}</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
|
@ -235,13 +249,13 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
onClick={handleUpdateAlias}
|
||||
className="text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded hover:bg-blue-100"
|
||||
>
|
||||
Save
|
||||
{t("common.save")}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelEdit}
|
||||
className="text-xs bg-gray-50 text-gray-600 px-2 py-1 rounded hover:bg-gray-100"
|
||||
>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
|
@ -273,7 +287,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
{aliases.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="py-0.5 text-sm text-gray-500 text-center">
|
||||
No aliases added yet. Add a new alias above.
|
||||
{t("commonComponents.modelAliasManager.noAliases")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
|
@ -285,8 +299,8 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
{/* Configuration Example */}
|
||||
{showExampleConfig && (
|
||||
<Card>
|
||||
<Title className="mb-4">Configuration Example</Title>
|
||||
<Text className="text-gray-600 mb-4">Here's how your current aliases would look in the config:</Text>
|
||||
<Title className="mb-4">{t("commonComponents.modelAliasManager.configExample")}</Title>
|
||||
<Text className="text-gray-600 mb-4">{t("commonComponents.modelAliasManager.configExampleDesc")}</Text>
|
||||
<div className="bg-gray-100 rounded-lg p-4 font-mono text-sm">
|
||||
<div className="text-gray-700">
|
||||
model_aliases:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from "react";
|
|||
import { TextInput, Text } from "@tremor/react";
|
||||
import { Select } from "antd";
|
||||
import { RobotOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models";
|
||||
|
||||
interface ModelSelectorProps {
|
||||
|
|
@ -19,14 +20,15 @@ interface ModelSelectorProps {
|
|||
const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||
accessToken,
|
||||
value,
|
||||
placeholder = "Select a Model",
|
||||
placeholder,
|
||||
onChange,
|
||||
disabled = false,
|
||||
style,
|
||||
className,
|
||||
showLabel = true,
|
||||
labelText = "Select Model",
|
||||
labelText,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedModel, setSelectedModel] = useState<string | undefined>(value);
|
||||
const [showCustomModelInput, setShowCustomModelInput] = useState<boolean>(false);
|
||||
const [modelInfo, setModelInfo] = useState<ModelGroup[]>([]);
|
||||
|
|
@ -86,12 +88,12 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
<div>
|
||||
{showLabel && (
|
||||
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
|
||||
<RobotOutlined className="mr-2" /> {labelText}
|
||||
<RobotOutlined className="mr-2" /> {labelText ?? t("commonComponents.modelSelector.labelText")}
|
||||
</Text>
|
||||
)}
|
||||
<Select
|
||||
value={selectedModel}
|
||||
placeholder={placeholder}
|
||||
placeholder={placeholder ?? t("commonComponents.modelSelector.placeholder")}
|
||||
onChange={onModelChange}
|
||||
options={[
|
||||
...Array.from(new Set(modelInfo.map((option) => option.model_group))).map((model_group, index) => ({
|
||||
|
|
@ -99,7 +101,7 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
label: model_group,
|
||||
key: index,
|
||||
})),
|
||||
{ value: "custom", label: "Enter custom model", key: "custom" },
|
||||
{ value: "custom", label: t("commonComponents.modelSelector.enterCustomModel"), key: "custom" },
|
||||
]}
|
||||
style={{ width: "100%", ...style }}
|
||||
showSearch={true}
|
||||
|
|
@ -109,7 +111,7 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||
{showCustomModelInput && (
|
||||
<TextInput
|
||||
className="mt-2"
|
||||
placeholder="Enter custom model name"
|
||||
placeholder={t("commonComponents.modelSelector.customModelPlaceholder")}
|
||||
onValueChange={handleCustomModelChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -197,6 +197,89 @@
|
|||
"rateLimit": "Rate Limit"
|
||||
}
|
||||
},
|
||||
"commonComponents": {
|
||||
"accessGroupSelector": {
|
||||
"placeholder": "Select access groups",
|
||||
"labelText": "Access Group",
|
||||
"loadFailed": "Failed to load access groups",
|
||||
"notFound": "No access groups found"
|
||||
},
|
||||
"autoRotationView": {
|
||||
"title": "Auto-Rotation",
|
||||
"subtitle": "Automatic key rotation settings and status for this key",
|
||||
"every": "Every {{interval}}",
|
||||
"lastRotation": "Last Rotation",
|
||||
"nextScheduledRotation": "Next Scheduled Rotation",
|
||||
"noRotationHistory": "No rotation history available",
|
||||
"notEnabled": "Auto-rotation is not enabled for this key"
|
||||
},
|
||||
"defaultProxyAdminTag": {
|
||||
"label": "Default Proxy Admin"
|
||||
},
|
||||
"deleteResourceModal": {
|
||||
"okText": "Delete",
|
||||
"typeToConfirm": "Type <danger>{{name}}</danger> to confirm deletion:"
|
||||
},
|
||||
"durationSelect": {
|
||||
"daily": "Daily",
|
||||
"weekly": "Weekly",
|
||||
"monthly": "Monthly"
|
||||
},
|
||||
"keyLifecycleSettings": {
|
||||
"keyExpirySectionTitle": "Key Expiry Settings",
|
||||
"expireKeyLabel": "Expire Key",
|
||||
"expireKeyTooltip": "Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Leave empty to keep the current expiry unchanged.",
|
||||
"neverExpire": "Never Expire",
|
||||
"expireKeyPlaceholderCreate": "e.g., 30d or leave empty to never expire",
|
||||
"expireKeyPlaceholderEdit": "e.g., 30d",
|
||||
"autoRotationSectionTitle": "Auto-Rotation Settings",
|
||||
"enableAutoRotationLabel": "Enable Auto-Rotation",
|
||||
"enableAutoRotationTooltip": "Key will automatically regenerate at the specified interval for enhanced security.",
|
||||
"rotationIntervalLabel": "Rotation Interval",
|
||||
"rotationIntervalTooltip": "How often the key should be automatically rotated. Choose the interval that best fits your security requirements.",
|
||||
"selectInterval": "Select interval",
|
||||
"sevenDays": "7 days",
|
||||
"thirtyDays": "30 days",
|
||||
"ninetyDays": "90 days",
|
||||
"oneHundredEightyDays": "180 days",
|
||||
"threeHundredSixtyFiveDays": "365 days",
|
||||
"customInterval": "Custom interval",
|
||||
"customIntervalPlaceholder": "e.g., 1s, 5m, 2h, 14d",
|
||||
"customIntervalFormats": "Supported formats: seconds (s), minutes (m), hours (h), days (d)",
|
||||
"rotationNotice": "When rotation occurs, you'll receive a notification with the new key. The old key will be deactivated after a brief grace period."
|
||||
},
|
||||
"memberTable": {
|
||||
"userEmail": "User Email",
|
||||
"userId": "User ID",
|
||||
"defaultProxyAdmin": "Default Proxy Admin",
|
||||
"memberCount_one": "{{count}} Member",
|
||||
"memberCount_other": "{{count}} Members",
|
||||
"editMember": "Edit member",
|
||||
"deleteMember": "Delete member",
|
||||
"addMember": "Add Member"
|
||||
},
|
||||
"modelAliasManager": {
|
||||
"bothRequired": "Please provide both alias name and target model",
|
||||
"duplicateAlias": "An alias with this name already exists",
|
||||
"addSuccess": "Alias added successfully",
|
||||
"updateSuccess": "Alias updated successfully",
|
||||
"deleteSuccess": "Alias deleted successfully",
|
||||
"addNewAlias": "Add New Alias",
|
||||
"aliasName": "Alias Name",
|
||||
"targetModel": "Target Model",
|
||||
"addAlias": "Add Alias",
|
||||
"manageExisting": "Manage Existing Aliases",
|
||||
"noAliases": "No aliases added yet. Add a new alias above.",
|
||||
"configExample": "Configuration Example",
|
||||
"configExampleDesc": "Here's how your current aliases would look in the config:"
|
||||
},
|
||||
"modelSelector": {
|
||||
"placeholder": "Select a Model",
|
||||
"labelText": "Select Model",
|
||||
"enterCustomModel": "Enter custom model",
|
||||
"customModelPlaceholder": "Enter custom model name"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
|
|
|
|||
|
|
@ -197,6 +197,89 @@
|
|||
"rateLimit": "速率限制"
|
||||
}
|
||||
},
|
||||
"commonComponents": {
|
||||
"accessGroupSelector": {
|
||||
"placeholder": "选择访问组",
|
||||
"labelText": "访问组",
|
||||
"loadFailed": "加载访问组失败",
|
||||
"notFound": "未找到访问组"
|
||||
},
|
||||
"autoRotationView": {
|
||||
"title": "自动轮换",
|
||||
"subtitle": "此密钥的自动轮换设置与状态",
|
||||
"every": "每 {{interval}}",
|
||||
"lastRotation": "上次轮换",
|
||||
"nextScheduledRotation": "下次计划轮换",
|
||||
"noRotationHistory": "暂无轮换历史",
|
||||
"notEnabled": "此密钥未启用自动轮换"
|
||||
},
|
||||
"defaultProxyAdminTag": {
|
||||
"label": "默认代理管理员"
|
||||
},
|
||||
"deleteResourceModal": {
|
||||
"okText": "删除",
|
||||
"typeToConfirm": "请输入 <danger>{{name}}</danger> 以确认删除:"
|
||||
},
|
||||
"durationSelect": {
|
||||
"daily": "每天",
|
||||
"weekly": "每周",
|
||||
"monthly": "每月"
|
||||
},
|
||||
"keyLifecycleSettings": {
|
||||
"keyExpirySectionTitle": "密钥过期设置",
|
||||
"expireKeyLabel": "密钥过期",
|
||||
"expireKeyTooltip": "设置此密钥的过期时间。格式:30s(秒)、30m(分钟)、30h(小时)、30d(天)。留空则保持当前过期时间不变。",
|
||||
"neverExpire": "永不过期",
|
||||
"expireKeyPlaceholderCreate": "例如:30d,留空则永不过期",
|
||||
"expireKeyPlaceholderEdit": "例如:30d",
|
||||
"autoRotationSectionTitle": "自动轮换设置",
|
||||
"enableAutoRotationLabel": "启用自动轮换",
|
||||
"enableAutoRotationTooltip": "密钥将按指定间隔自动重新生成,以增强安全性。",
|
||||
"rotationIntervalLabel": "轮换间隔",
|
||||
"rotationIntervalTooltip": "密钥自动轮换的频率。请选择最适合您安全要求的间隔。",
|
||||
"selectInterval": "选择间隔",
|
||||
"sevenDays": "7 天",
|
||||
"thirtyDays": "30 天",
|
||||
"ninetyDays": "90 天",
|
||||
"oneHundredEightyDays": "180 天",
|
||||
"threeHundredSixtyFiveDays": "365 天",
|
||||
"customInterval": "自定义间隔",
|
||||
"customIntervalPlaceholder": "例如:1s、5m、2h、14d",
|
||||
"customIntervalFormats": "支持格式:秒(s)、分钟(m)、小时(h)、天(d)",
|
||||
"rotationNotice": "轮换发生时,您将收到包含新密钥的通知。旧密钥将在短暂的宽限期后停用。"
|
||||
},
|
||||
"memberTable": {
|
||||
"userEmail": "用户邮箱",
|
||||
"userId": "用户 ID",
|
||||
"defaultProxyAdmin": "默认代理管理员",
|
||||
"memberCount_one": "{{count}} 位成员",
|
||||
"memberCount_other": "{{count}} 位成员",
|
||||
"editMember": "编辑成员",
|
||||
"deleteMember": "删除成员",
|
||||
"addMember": "添加成员"
|
||||
},
|
||||
"modelAliasManager": {
|
||||
"bothRequired": "请同时提供别名名称和目标模型",
|
||||
"duplicateAlias": "同名别名已存在",
|
||||
"addSuccess": "别名添加成功",
|
||||
"updateSuccess": "别名更新成功",
|
||||
"deleteSuccess": "别名删除成功",
|
||||
"addNewAlias": "添加新别名",
|
||||
"aliasName": "别名名称",
|
||||
"targetModel": "目标模型",
|
||||
"addAlias": "添加别名",
|
||||
"manageExisting": "管理现有别名",
|
||||
"noAliases": "暂无别名。请在上方添加新别名。",
|
||||
"configExample": "配置示例",
|
||||
"configExampleDesc": "以下是您当前别名在配置中的样子:"
|
||||
},
|
||||
"modelSelector": {
|
||||
"placeholder": "选择模型",
|
||||
"labelText": "选择模型",
|
||||
"enterCustomModel": "输入自定义模型",
|
||||
"customModelPlaceholder": "输入自定义模型名称"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue