feat(ui): i18n for pass-through settings, per-user usage, price reload, and prompts

This commit is contained in:
吹雪 2026-06-10 13:51:24 +09:00
parent 38f2137eca
commit b03c393a54
6 changed files with 286 additions and 101 deletions

View file

@ -9,6 +9,7 @@ import { DataTable } from "./view_logs/table";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, EyeOff } from "lucide-react";
import NotificationsManager from "./molecules/notifications_manager";
import { useTranslation } from "react-i18next";
interface GeneralSettingsPageProps {
accessToken: string | null;
@ -66,6 +67,7 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
modelData,
premiumUser,
}) => {
const { t } = useTranslation();
const [generalSettings, setGeneralSettings] = useState<passThroughItem[]>([]);
const [selectedEndpointId, setSelectedEndpointId] = useState<string | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@ -108,10 +110,10 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
const updatedSettings = generalSettings.filter((setting) => setting.id !== endpointToDelete);
setGeneralSettings(updatedSettings);
NotificationsManager.success("Endpoint deleted successfully.");
NotificationsManager.success(t("passThroughSettings.deleteSuccess"));
} catch (error) {
console.error("Error deleting the endpoint:", error);
NotificationsManager.fromBackend("Error deleting the endpoint: " + error);
NotificationsManager.fromBackend(t("passThroughSettings.deleteFailed", { error }));
}
// Close the confirmation modal and reset the endpointToDelete
@ -133,7 +135,7 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
// Define columns for the DataTable
const columns: ColumnDef<passThroughItem>[] = [
{
header: "ID",
header: t("passThroughSettings.colId"),
accessorKey: "id",
cell: (info: any) => (
<Tooltip title={info.row.original.id}>
@ -147,19 +149,19 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
),
},
{
header: "Path",
header: t("passThroughSettings.colPath"),
accessorKey: "path",
},
{
header: "Target",
header: t("passThroughSettings.colTarget"),
accessorKey: "target",
cell: (info: any) => <Text>{info.getValue()}</Text>,
},
{
header: () => (
<div className="flex items-center gap-1">
<span>Methods</span>
<Tooltip title="HTTP methods supported by this endpoint">
<span>{t("passThroughSettings.colMethods")}</span>
<Tooltip title={t("passThroughSettings.methodsTooltip")}>
<InformationCircleIcon className="w-4 h-4 text-gray-400 cursor-help" />
</Tooltip>
</div>
@ -184,22 +186,24 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
{
header: () => (
<div className="flex items-center gap-1">
<span>Authentication</span>
<Tooltip title="LiteLLM Virtual Key required to call endpoint">
<span>{t("passThroughSettings.colAuthentication")}</span>
<Tooltip title={t("passThroughSettings.authTooltip")}>
<InformationCircleIcon className="w-4 h-4 text-gray-400 cursor-help" />
</Tooltip>
</div>
),
accessorKey: "auth",
cell: (info: any) => <Badge color={info.getValue() ? "green" : "gray"}>{info.getValue() ? "Yes" : "No"}</Badge>,
cell: (info: any) => (
<Badge color={info.getValue() ? "green" : "gray"}>{info.getValue() ? t("common.yes") : t("common.no")}</Badge>
),
},
{
header: "Headers",
header: t("passThroughSettings.colHeaders"),
accessorKey: "headers",
cell: (info: any) => <PasswordField value={info.getValue() || {}} />,
},
{
header: "Actions",
header: t("common.actions"),
id: "actions",
cell: ({ row }) => (
<div className="flex space-x-1">
@ -207,13 +211,13 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
icon={PencilAltIcon}
size="sm"
onClick={() => row.original.id && setSelectedEndpointId(row.original.id)}
title="Edit"
title={t("common.edit")}
/>
<Icon
icon={TrashIcon}
size="sm"
onClick={() => handleResetField(row.original.id!, row.index)}
title="Delete"
title={t("common.delete")}
/>
</div>
),
@ -232,7 +236,7 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
const selectedEndpoint = generalSettings.find((endpoint) => endpoint.id === selectedEndpointId);
if (!selectedEndpoint) {
return <div>Endpoint not found</div>;
return <div>{t("passThroughSettings.endpointNotFound")}</div>;
}
return (
@ -250,8 +254,8 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
return (
<div>
<div>
<Title>Pass Through Endpoints</Title>
<Text className="text-tremor-content">Configure and manage your pass-through endpoints</Text>
<Title>{t("passThroughSettings.title")}</Title>
<Text className="text-tremor-content">{t("passThroughSettings.subtitle")}</Text>
</div>
<AddPassThroughEndpoint
@ -267,7 +271,7 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
renderSubComponent={() => <div></div>}
getRowCanExpand={() => false}
isLoading={false}
noDataMessage="No pass-through endpoints configured"
noDataMessage={t("passThroughSettings.noData")}
/>
{isDeleteModalOpen && (
@ -287,20 +291,20 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900">Delete Pass-Through Endpoint</h3>
<h3 className="text-lg leading-6 font-medium text-gray-900">
{t("passThroughSettings.deleteModalTitle")}
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to delete this pass-through endpoint? This action cannot be undone.
</p>
<p className="text-sm text-gray-500">{t("passThroughSettings.deleteModalMessage")}</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<Button onClick={confirmDelete} color="red" className="ml-2">
Delete
{t("common.delete")}
</Button>
<Button onClick={cancelDelete}>Cancel</Button>
<Button onClick={cancelDelete}>{t("common.cancel")}</Button>
</div>
</div>
</div>

View file

@ -18,6 +18,7 @@ import {
TabPanels,
} from "@tremor/react";
import { perUserAnalyticsCall } from "./networking";
import { useTranslation } from "react-i18next";
interface PerUserMetrics {
user_id: string;
@ -45,6 +46,7 @@ interface PerUserUsageProps {
}
const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags, formatAbbreviatedNumber }) => {
const { t } = useTranslation();
// Maximum number of user agent categories to show in charts to prevent color palette overflow
const MAX_USER_AGENTS = 8;
const [perUserData, setPerUserData] = useState<PerUserAnalyticsResponse>({
@ -95,13 +97,13 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
return (
<div className="mb-6">
<Title>Per User Usage</Title>
<Subtitle>Individual developer usage metrics</Subtitle>
<Title>{t("perUserUsage.title")}</Title>
<Subtitle>{t("perUserUsage.subtitle")}</Subtitle>
<TabGroup>
<TabList className="mb-6">
<Tab>User Details</Tab>
<Tab>Usage Distribution</Tab>
<Tab>{t("perUserUsage.tabUserDetails")}</Tab>
<Tab>{t("perUserUsage.tabUsageDistribution")}</Tab>
</TabList>
<TabPanels>
@ -110,13 +112,13 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>User ID</TableHeaderCell>
<TableHeaderCell>User Email</TableHeaderCell>
<TableHeaderCell>User Agent</TableHeaderCell>
<TableHeaderCell className="text-right">Success Generations</TableHeaderCell>
<TableHeaderCell className="text-right">Total Tokens</TableHeaderCell>
<TableHeaderCell className="text-right">Failed Requests</TableHeaderCell>
<TableHeaderCell className="text-right">Total Cost</TableHeaderCell>
<TableHeaderCell>{t("perUserUsage.colUserId")}</TableHeaderCell>
<TableHeaderCell>{t("perUserUsage.colUserEmail")}</TableHeaderCell>
<TableHeaderCell>{t("perUserUsage.colUserAgent")}</TableHeaderCell>
<TableHeaderCell className="text-right">{t("perUserUsage.colSuccessGenerations")}</TableHeaderCell>
<TableHeaderCell className="text-right">{t("perUserUsage.colTotalTokens")}</TableHeaderCell>
<TableHeaderCell className="text-right">{t("perUserUsage.colFailedRequests")}</TableHeaderCell>
<TableHeaderCell className="text-right">{t("perUserUsage.colTotalCost")}</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
@ -126,10 +128,10 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
<Text className="font-medium">{item.user_id}</Text>
</TableCell>
<TableCell>
<Text>{item.user_email || "N/A"}</Text>
<Text>{item.user_email || t("perUserUsage.naValue")}</Text>
</TableCell>
<TableCell>
<Text>{item.user_agent || "Unknown"}</Text>
<Text>{item.user_agent || t("common.unknown")}</Text>
</TableCell>
<TableCell className="text-right">
<Text>{formatAbbreviatedNumber(item.successful_requests)}</Text>
@ -150,10 +152,12 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
{perUserData.results.length > 10 && (
<div className="mt-4 flex justify-between items-center">
<Text className="text-sm text-gray-500">Showing 10 of {perUserData.total_count} results</Text>
<Text className="text-sm text-gray-500">
{t("perUserUsage.showing10Of", { total: perUserData.total_count })}
</Text>
<div className="flex gap-2">
<Button size="sm" variant="secondary" onClick={handlePrevPage} disabled={currentPage === 1}>
Previous
{t("common.previous")}
</Button>
<Button
size="sm"
@ -161,7 +165,7 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
onClick={handleNextPage}
disabled={currentPage >= perUserData.total_pages}
>
Next
{t("common.next")}
</Button>
</div>
</div>
@ -171,8 +175,8 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
{/* Tab 2: Usage Distribution Histogram */}
<TabPanel>
<div className="mb-4">
<Title className="text-lg">User Usage Distribution</Title>
<Subtitle>Number of users by successful request frequency</Subtitle>
<Title className="text-lg">{t("perUserUsage.distributionTitle")}</Title>
<Subtitle>{t("perUserUsage.distributionSubtitle")}</Subtitle>
</div>
<BarChart
@ -180,7 +184,7 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
// Get top user agents by frequency first
const userAgentCounts = new Map<string, number>();
perUserData.results.forEach((item: PerUserMetrics) => {
const agent = item.user_agent || "Unknown";
const agent = item.user_agent || t("common.unknown");
userAgentCounts.set(agent, (userAgentCounts.get(agent) || 0) + 1);
});
@ -234,7 +238,7 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
// Count user agents by frequency and get top ones
const userAgentCounts = new Map<string, number>();
perUserData.results.forEach((item: PerUserMetrics) => {
const agent = item.user_agent || "Unknown";
const agent = item.user_agent || t("common.unknown");
userAgentCounts.set(agent, (userAgentCounts.get(agent) || 0) + 1);
});
@ -245,7 +249,7 @@ const PerUserUsage: React.FC<PerUserUsageProps> = ({ accessToken, selectedTags,
.map(([agent]) => agent);
})()}
colors={["blue", "green", "orange", "red", "purple", "yellow", "pink", "indigo"]}
valueFormatter={(value: number) => `${value} users`}
valueFormatter={(value: number) => t("perUserUsage.usersCount", { count: value })}
yAxisWidth={80}
showLegend={true}
stack={true}

View file

@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Button, Popconfirm, Modal, InputNumber, Space, Typography, Tag, Card, Tooltip, Divider } from "antd";
import {
ReloadOutlined,
@ -48,12 +49,14 @@ interface PriceDataReloadProps {
const PriceDataReload: React.FC<PriceDataReloadProps> = ({
accessToken,
onReloadSuccess,
buttonText = "Reload Price Data",
buttonText,
showIcon = true,
size = "middle",
type = "primary",
className = "",
}) => {
const { t } = useTranslation();
const resolvedButtonText = buttonText ?? t("priceDataReload.reloadButton");
const [isLoading, setIsLoading] = useState(false);
const [isScheduling, setIsScheduling] = useState(false);
const [isCancelling, setIsCancelling] = useState(false);
@ -117,7 +120,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
const handleHardRefresh = async () => {
if (!accessToken) {
NotificationsManager.fromBackend("No access token available");
NotificationsManager.fromBackend(t("priceDataReload.noAccessToken"));
return;
}
@ -126,29 +129,29 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
const response = await reloadModelCostMap(accessToken);
if (response.status === "success") {
NotificationsManager.success(`Price data reloaded successfully! ${response.models_count || 0} models updated.`);
NotificationsManager.success(t("priceDataReload.reloadSuccess", { count: response.models_count || 0 }));
onReloadSuccess?.();
// Refresh status and source info after successful reload
await fetchReloadStatus();
await fetchSourceInfo();
} else {
NotificationsManager.fromBackend("Failed to reload price data");
NotificationsManager.fromBackend(t("priceDataReload.reloadFailed"));
}
} catch (error) {
console.error("Error reloading price data:", error);
NotificationsManager.fromBackend("Failed to reload price data. Please try again.");
NotificationsManager.fromBackend(t("priceDataReload.reloadFailedRetry"));
} finally {
setIsLoading(false);
}
};
const handleScheduleReload = async () => {
if (!accessToken) {
NotificationsManager.fromBackend("No access token available");
NotificationsManager.fromBackend(t("priceDataReload.noAccessToken"));
return;
}
if (hours <= 0) {
NotificationsManager.fromBackend("Hours must be greater than 0");
NotificationsManager.fromBackend(t("priceDataReload.hoursMustBePositive"));
return;
}
@ -157,15 +160,15 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
const response = await scheduleModelCostMapReload(accessToken, hours);
if (response.status === "success") {
NotificationsManager.success(`Periodic reload scheduled for every ${hours} hours`);
NotificationsManager.success(t("priceDataReload.scheduleSuccess", { hours }));
setShowScheduleModal(false);
await fetchReloadStatus();
} else {
NotificationsManager.fromBackend("Failed to schedule periodic reload");
NotificationsManager.fromBackend(t("priceDataReload.scheduleFailed"));
}
} catch (error) {
console.error("Error scheduling reload:", error);
NotificationsManager.fromBackend("Failed to schedule periodic reload. Please try again.");
NotificationsManager.fromBackend(t("priceDataReload.scheduleFailedRetry"));
} finally {
setIsScheduling(false);
}
@ -173,7 +176,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
const handleCancelReload = async () => {
if (!accessToken) {
NotificationsManager.fromBackend("No access token available");
NotificationsManager.fromBackend(t("priceDataReload.noAccessToken"));
return;
}
@ -182,21 +185,21 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
const response = await cancelModelCostMapReload(accessToken);
if (response.status === "success") {
NotificationsManager.success("Periodic reload cancelled successfully");
NotificationsManager.success(t("priceDataReload.cancelSuccess"));
await fetchReloadStatus();
} else {
NotificationsManager.fromBackend("Failed to cancel periodic reload");
NotificationsManager.fromBackend(t("priceDataReload.cancelFailed"));
}
} catch (error) {
console.error("Error cancelling reload:", error);
NotificationsManager.fromBackend("Failed to cancel periodic reload. Please try again.");
NotificationsManager.fromBackend(t("priceDataReload.cancelFailedRetry"));
} finally {
setIsCancelling(false);
}
};
const formatDateTime = (dateTimeString: string | null) => {
if (!dateTimeString) return "Never";
if (!dateTimeString) return t("common.never");
try {
return new Date(dateTimeString).toLocaleString();
} catch {
@ -205,9 +208,9 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
};
const getStatusText = () => {
if (!reloadStatus?.scheduled) return "Not scheduled";
if (!reloadStatus.last_run) return "Ready";
return "Active";
if (!reloadStatus?.scheduled) return t("priceDataReload.statusNotScheduled");
if (!reloadStatus.last_run) return t("priceDataReload.statusReady");
return t("priceDataReload.statusActive");
};
const getStatusColor = () => {
@ -222,11 +225,11 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
<Space direction="horizontal" size="middle" style={{ marginBottom: 16 }}>
{/* Hard Refresh Button - Always visible */}
<Popconfirm
title="Hard Refresh Price Data"
description="This will immediately fetch the latest pricing information from the remote source. Continue?"
title={t("priceDataReload.hardRefreshTitle")}
description={t("priceDataReload.hardRefreshDesc")}
onConfirm={handleHardRefresh}
okText="Yes"
cancelText="No"
okText={t("common.yes")}
cancelText={t("common.no")}
okButtonProps={{
style: {
backgroundColor: "#6366f1",
@ -272,7 +275,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
e.currentTarget.style.backgroundColor = "#6366f1";
}}
>
{buttonText}
{resolvedButtonText}
</Button>
</Popconfirm>
@ -294,7 +297,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
lineHeight: "1.25rem",
}}
>
Set Up Periodic Reload
{t("priceDataReload.setupPeriodicReload")}
</Button>
) : (
<Button
@ -315,7 +318,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
lineHeight: "1.25rem",
}}
>
Cancel Periodic Reload
{t("priceDataReload.cancelPeriodicReload")}
</Button>
)}
</Space>
@ -340,13 +343,13 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
<DatabaseOutlined style={{ color: "#fa8c16", fontSize: 16 }} />
)}
<Text strong style={{ fontSize: "13px" }}>
Pricing Data Source
{t("priceDataReload.pricingDataSource")}
</Text>
<Tag
color={sourceInfo.source === "remote" ? "blue" : "orange"}
style={{ marginLeft: "auto", fontWeight: 600, textTransform: "uppercase", fontSize: "11px" }}
>
{sourceInfo.source === "remote" ? "Remote" : "Local"}
{sourceInfo.source === "remote" ? t("priceDataReload.sourceRemote") : t("priceDataReload.sourceLocal")}
</Tag>
</div>
@ -355,7 +358,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
{/* Model count */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Text type="secondary" style={{ fontSize: "12px" }}>
Models loaded:
{t("priceDataReload.modelsLoaded")}
</Text>
<Text strong style={{ fontSize: "12px" }}>
{sourceInfo.model_count.toLocaleString()}
@ -366,7 +369,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
{sourceInfo.url && (
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 8 }}>
<Text type="secondary" style={{ fontSize: "12px", whiteSpace: "nowrap" }}>
{sourceInfo.source === "remote" ? "Loaded from:" : "Attempted URL:"}
{sourceInfo.source === "remote" ? t("priceDataReload.loadedFrom") : t("priceDataReload.attemptedUrl")}
</Text>
<Tooltip title={sourceInfo.url}>
<Text
@ -392,7 +395,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
<div style={{ display: "flex", alignItems: "center", gap: 6, marginTop: 2 }}>
<InfoCircleOutlined style={{ color: "#fa8c16", fontSize: 12 }} />
<Text type="secondary" style={{ fontSize: "11px" }}>
Local mode forced via <code>LITELLM_LOCAL_MODEL_COST_MAP=True</code>
{t("priceDataReload.localModeForced")}
</Text>
</div>
)}
@ -413,7 +416,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
>
<WarningOutlined style={{ color: "#fa8c16", fontSize: 12, marginTop: 2 }} />
<Text style={{ fontSize: "11px", color: "#614700" }}>
Fell back to local: {sourceInfo.fallback_reason}
{t("priceDataReload.fellBackToLocal", { reason: sourceInfo.fallback_reason })}
</Text>
</div>
)}
@ -435,16 +438,16 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
{reloadStatus.scheduled ? (
<div>
<Tag color="green" icon={<ClockCircleOutlined />}>
Scheduled every {reloadStatus.interval_hours} hours
{t("priceDataReload.scheduledEvery", { hours: reloadStatus.interval_hours })}
</Tag>
</div>
) : (
<Text type="secondary">No periodic reload scheduled</Text>
<Text type="secondary">{t("priceDataReload.noPeriodicReload")}</Text>
)}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Text type="secondary" style={{ fontSize: "12px" }}>
Last run:
{t("priceDataReload.lastRun")}
</Text>
<Text style={{ fontSize: "12px" }}>{formatDateTime(reloadStatus.last_run)}</Text>
</div>
@ -454,14 +457,14 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
{reloadStatus.next_run && (
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Text type="secondary" style={{ fontSize: "12px" }}>
Next run:
{t("priceDataReload.nextRun")}
</Text>
<Text style={{ fontSize: "12px" }}>{formatDateTime(reloadStatus.next_run)}</Text>
</div>
)}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Text type="secondary" style={{ fontSize: "12px" }}>
Status:
{t("common.status")}
</Text>
<Tag color={getStatusColor()}>{getStatusText()}</Tag>
</div>
@ -473,13 +476,13 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
{/* Schedule Modal */}
<Modal
title="Set Up Periodic Reload"
title={t("priceDataReload.scheduleModalTitle")}
open={showScheduleModal}
onOk={handleScheduleReload}
onCancel={() => setShowScheduleModal(false)}
confirmLoading={isScheduling}
okText="Schedule"
cancelText="Cancel"
okText={t("priceDataReload.scheduleButton")}
cancelText={t("common.cancel")}
okButtonProps={{
style: {
backgroundColor: "#6366f1",
@ -489,7 +492,7 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
}}
>
<div style={{ marginBottom: 16 }}>
<Text>Set up automatic reload of price data every:</Text>
<Text>{t("priceDataReload.scheduleModalDesc")}</Text>
</div>
<div style={{ marginBottom: 16 }}>
<InputNumber
@ -497,14 +500,12 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
max={168} // 1 week max
value={hours}
onChange={(value) => setHours(value || 6)}
addonAfter="hours"
addonAfter={t("priceDataReload.hoursLabel")}
style={{ width: "100%" }}
/>
</div>
<div>
<Text type="secondary">
This will automatically fetch the latest pricing data from the remote source every {hours} hours.
</Text>
<Text type="secondary">{t("priceDataReload.scheduleModalHint", { hours })}</Text>
</div>
</Modal>
</div>

View file

@ -9,6 +9,7 @@ import AddPromptForm from "./prompts/add_prompt_form";
import PromptEditorView from "./prompts/prompt_editor_view";
import NotificationsManager from "./molecules/notifications_manager";
import { isAdminRole, isProxyAdminRole } from "@/utils/roles";
import { useTranslation } from "react-i18next";
interface PromptsProps {
accessToken: string | null;
@ -16,6 +17,7 @@ interface PromptsProps {
}
const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
const { t } = useTranslation();
const [promptsList, setPromptsList] = useState<PromptSpec[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [selectedEnvironment, setSelectedEnvironment] = useState<string | undefined>(undefined);
@ -101,11 +103,11 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
setIsDeleting(true);
try {
await deletePromptCall(accessToken, promptToDelete.id);
NotificationsManager.success(`Prompt "${promptToDelete.name}" deleted successfully`);
NotificationsManager.success(t("prompts.deleteSuccess", { name: promptToDelete.name }));
fetchPrompts(); // Refresh the list
} catch (error) {
console.error("Error deleting prompt:", error);
NotificationsManager.fromBackend("Failed to delete prompt");
NotificationsManager.fromBackend(t("prompts.deleteFailed"));
} finally {
setIsDeleting(false);
setPromptToDelete(null);
@ -141,24 +143,24 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
{canModify && (
<>
<Button onClick={handleAddPrompt} disabled={!accessToken}>
+ Add New Prompt
{t("prompts.addNewPrompt")}
</Button>
<Button onClick={handleAddPromptFromFile} disabled={!accessToken} variant="secondary">
Upload .prompt File
{t("prompts.uploadPromptFile")}
</Button>
</>
)}
</div>
<Select
placeholder="All Environments"
placeholder={t("prompts.allEnvironments")}
allowClear
value={selectedEnvironment}
onChange={(value) => setSelectedEnvironment(value)}
style={{ width: 180 }}
options={[
{ label: "Development", value: "development" },
{ label: "Staging", value: "staging" },
{ label: "Production", value: "production" },
{ label: t("prompts.envDevelopment"), value: "development" },
{ label: t("prompts.envStaging"), value: "staging" },
{ label: t("prompts.envProduction"), value: "production" },
]}
/>
</div>
@ -183,16 +185,16 @@ const PromptsPanel: React.FC<PromptsProps> = ({ accessToken, userRole }) => {
{promptToDelete && (
<Modal
title="Delete Prompt"
title={t("prompts.deleteModalTitle")}
open={promptToDelete !== null}
onOk={handleDeleteConfirm}
onCancel={handleDeleteCancel}
confirmLoading={isDeleting}
okText="Delete"
okText={t("common.delete")}
okButtonProps={{ danger: true }}
>
<p>Are you sure you want to delete prompt: {promptToDelete.name} ?</p>
<p>This action cannot be undone.</p>
<p>{t("prompts.deleteConfirm", { name: promptToDelete.name })}</p>
<p>{t("prompts.deleteCannotUndo")}</p>
</Modal>
)}
</div>

View file

@ -1530,6 +1530,93 @@
"deleteSuccess": "Organization deleted successfully",
"createSuccess": "Organization created successfully"
},
"passThroughSettings": {
"title": "Pass Through Endpoints",
"subtitle": "Configure and manage your pass-through endpoints",
"colId": "ID",
"colPath": "Path",
"colTarget": "Target",
"colMethods": "Methods",
"methodsTooltip": "HTTP methods supported by this endpoint",
"colAuthentication": "Authentication",
"authTooltip": "LiteLLM Virtual Key required to call endpoint",
"colHeaders": "Headers",
"noData": "No pass-through endpoints configured",
"endpointNotFound": "Endpoint not found",
"deleteModalTitle": "Delete Pass-Through Endpoint",
"deleteModalMessage": "Are you sure you want to delete this pass-through endpoint? This action cannot be undone.",
"deleteSuccess": "Endpoint deleted successfully.",
"deleteFailed": "Error deleting the endpoint: {{error}}"
},
"perUserUsage": {
"title": "Per User Usage",
"subtitle": "Individual developer usage metrics",
"tabUserDetails": "User Details",
"tabUsageDistribution": "Usage Distribution",
"colUserId": "User ID",
"colUserEmail": "User Email",
"colUserAgent": "User Agent",
"colSuccessGenerations": "Success Generations",
"colTotalTokens": "Total Tokens",
"colFailedRequests": "Failed Requests",
"colTotalCost": "Total Cost",
"naValue": "N/A",
"showing10Of": "Showing 10 of {{total}} results",
"distributionTitle": "User Usage Distribution",
"distributionSubtitle": "Number of users by successful request frequency",
"usersCount": "{{count}} users"
},
"priceDataReload": {
"reloadButton": "Reload Price Data",
"noAccessToken": "No access token available",
"reloadSuccess": "Price data reloaded successfully! {{count}} models updated.",
"reloadFailed": "Failed to reload price data",
"reloadFailedRetry": "Failed to reload price data. Please try again.",
"hoursMustBePositive": "Hours must be greater than 0",
"scheduleSuccess": "Periodic reload scheduled for every {{hours}} hours",
"scheduleFailed": "Failed to schedule periodic reload",
"scheduleFailedRetry": "Failed to schedule periodic reload. Please try again.",
"cancelSuccess": "Periodic reload cancelled successfully",
"cancelFailed": "Failed to cancel periodic reload",
"cancelFailedRetry": "Failed to cancel periodic reload. Please try again.",
"statusNotScheduled": "Not scheduled",
"statusReady": "Ready",
"statusActive": "Active",
"hardRefreshTitle": "Hard Refresh Price Data",
"hardRefreshDesc": "This will immediately fetch the latest pricing information from the remote source. Continue?",
"setupPeriodicReload": "Set Up Periodic Reload",
"cancelPeriodicReload": "Cancel Periodic Reload",
"pricingDataSource": "Pricing Data Source",
"sourceRemote": "Remote",
"sourceLocal": "Local",
"modelsLoaded": "Models loaded:",
"loadedFrom": "Loaded from:",
"attemptedUrl": "Attempted URL:",
"localModeForced": "Local mode forced via LITELLM_LOCAL_MODEL_COST_MAP=True",
"fellBackToLocal": "Fell back to local: {{reason}}",
"scheduledEvery": "Scheduled every {{hours}} hours",
"noPeriodicReload": "No periodic reload scheduled",
"lastRun": "Last run:",
"nextRun": "Next run:",
"scheduleModalTitle": "Set Up Periodic Reload",
"scheduleButton": "Schedule",
"scheduleModalDesc": "Set up automatic reload of price data every:",
"hoursLabel": "hours",
"scheduleModalHint": "This will automatically fetch the latest pricing data from the remote source every {{hours}} hours."
},
"prompts": {
"addNewPrompt": "+ Add New Prompt",
"uploadPromptFile": "Upload .prompt File",
"allEnvironments": "All Environments",
"envDevelopment": "Development",
"envStaging": "Staging",
"envProduction": "Production",
"deleteModalTitle": "Delete Prompt",
"deleteConfirm": "Are you sure you want to delete prompt: {{name}} ?",
"deleteCannotUndo": "This action cannot be undone.",
"deleteSuccess": "Prompt \"{{name}}\" deleted successfully",
"deleteFailed": "Failed to delete prompt"
},
"passThroughInfo": {
"notFound": "Pass through endpoint not found",
"backButton": "← Back",

View file

@ -1530,6 +1530,93 @@
"deleteSuccess": "组织已成功删除",
"createSuccess": "组织已成功创建"
},
"passThroughSettings": {
"title": "透传端点",
"subtitle": "配置并管理您的透传端点",
"colId": "ID",
"colPath": "路径",
"colTarget": "目标",
"colMethods": "方法",
"methodsTooltip": "此端点支持的 HTTP 方法",
"colAuthentication": "认证",
"authTooltip": "调用端点需要 LiteLLM 虚拟密钥",
"colHeaders": "请求头",
"noData": "未配置透传端点",
"endpointNotFound": "未找到端点",
"deleteModalTitle": "删除透传端点",
"deleteModalMessage": "确定要删除此透传端点吗?此操作无法撤销。",
"deleteSuccess": "端点已成功删除。",
"deleteFailed": "删除端点时出错:{{error}}"
},
"perUserUsage": {
"title": "每用户用量",
"subtitle": "开发者个人用量指标",
"tabUserDetails": "用户详情",
"tabUsageDistribution": "用量分布",
"colUserId": "用户 ID",
"colUserEmail": "用户邮箱",
"colUserAgent": "User Agent",
"colSuccessGenerations": "成功生成次数",
"colTotalTokens": "总 Token 数",
"colFailedRequests": "失败请求数",
"colTotalCost": "总费用",
"naValue": "N/A",
"showing10Of": "显示第 10 条,共 {{total}} 条",
"distributionTitle": "用户用量分布",
"distributionSubtitle": "按成功请求频率统计的用户数量",
"usersCount": "{{count}} 个用户"
},
"priceDataReload": {
"reloadButton": "重新加载价格数据",
"noAccessToken": "无可用访问令牌",
"reloadSuccess": "价格数据已成功重新加载!已更新 {{count}} 个模型。",
"reloadFailed": "重新加载价格数据失败",
"reloadFailedRetry": "重新加载价格数据失败,请重试。",
"hoursMustBePositive": "小时数必须大于 0",
"scheduleSuccess": "已设置每 {{hours}} 小时定期重新加载",
"scheduleFailed": "设置定期重新加载失败",
"scheduleFailedRetry": "设置定期重新加载失败,请重试。",
"cancelSuccess": "定期重新加载已成功取消",
"cancelFailed": "取消定期重新加载失败",
"cancelFailedRetry": "取消定期重新加载失败,请重试。",
"statusNotScheduled": "未设置计划",
"statusReady": "就绪",
"statusActive": "活跃",
"hardRefreshTitle": "硬刷新价格数据",
"hardRefreshDesc": "这将立即从远程源获取最新定价信息。是否继续?",
"setupPeriodicReload": "设置定期重新加载",
"cancelPeriodicReload": "取消定期重新加载",
"pricingDataSource": "定价数据来源",
"sourceRemote": "远程",
"sourceLocal": "本地",
"modelsLoaded": "已加载模型数:",
"loadedFrom": "加载来源:",
"attemptedUrl": "尝试的 URL:",
"localModeForced": "已通过 LITELLM_LOCAL_MODEL_COST_MAP=True 强制使用本地模式",
"fellBackToLocal": "已回退到本地:{{reason}}",
"scheduledEvery": "每 {{hours}} 小时定期执行",
"noPeriodicReload": "未设置定期重新加载",
"lastRun": "上次运行:",
"nextRun": "下次运行:",
"scheduleModalTitle": "设置定期重新加载",
"scheduleButton": "设置计划",
"scheduleModalDesc": "设置每隔以下时间自动重新加载价格数据:",
"hoursLabel": "小时",
"scheduleModalHint": "系统将每 {{hours}} 小时自动从远程源获取最新定价数据。"
},
"prompts": {
"addNewPrompt": "+ 添加新提示词",
"uploadPromptFile": "上传 .prompt 文件",
"allEnvironments": "所有环境",
"envDevelopment": "开发环境",
"envStaging": "预发环境",
"envProduction": "生产环境",
"deleteModalTitle": "删除提示词",
"deleteConfirm": "确定要删除提示词:{{name}} 吗?",
"deleteCannotUndo": "此操作无法撤销。",
"deleteSuccess": "提示词「{{name}}」已成功删除",
"deleteFailed": "删除提示词失败"
},
"passThroughInfo": {
"notFound": "未找到透传端点",
"backButton": "← 返回",