mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for view_logs batch 1
This commit is contained in:
parent
e93492321a
commit
e6704034de
8 changed files with 410 additions and 129 deletions
|
|
@ -2,6 +2,7 @@ import { Drawer, Tag, Typography } from "antd";
|
|||
import { CloseOutlined, CopyOutlined, CheckOutlined } from "@ant-design/icons";
|
||||
import { useState, useCallback } from "react";
|
||||
import moment from "moment";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AuditLogEntry } from "../columns";
|
||||
import DefaultProxyAdminTag from "../../common_components/DefaultProxyAdminTag";
|
||||
|
||||
|
|
@ -13,14 +14,6 @@ interface AuditLogDrawerProps {
|
|||
log: AuditLogEntry | null;
|
||||
}
|
||||
|
||||
const TABLE_NAME_DISPLAY: Record<string, string> = {
|
||||
LiteLLM_VerificationToken: "Keys",
|
||||
LiteLLM_TeamTable: "Teams",
|
||||
LiteLLM_UserTable: "Users",
|
||||
LiteLLM_OrganizationTable: "Organizations",
|
||||
LiteLLM_ProxyModelTable: "Models",
|
||||
};
|
||||
|
||||
const ACTION_COLOR: Record<string, string> = {
|
||||
created: "green",
|
||||
updated: "blue",
|
||||
|
|
@ -29,6 +22,7 @@ const ACTION_COLOR: Record<string, string> = {
|
|||
};
|
||||
|
||||
function CopyableJsonBlock({ label, value }: { label: string; value: Record<string, any> }) {
|
||||
const { t } = useTranslation();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = useCallback(async () => {
|
||||
|
|
@ -61,7 +55,7 @@ function CopyableJsonBlock({ label, value }: { label: string; value: Record<stri
|
|||
<button
|
||||
onClick={handleCopy}
|
||||
className="p-1 hover:bg-gray-200 rounded text-gray-500 hover:text-gray-700 transition-colors"
|
||||
title="Copy JSON"
|
||||
title={t("viewLogs.auditLogDrawer.copyJson")}
|
||||
>
|
||||
{copied ? <CheckOutlined className="text-green-600" /> : <CopyOutlined />}
|
||||
</button>
|
||||
|
|
@ -83,6 +77,7 @@ function MetadataRow({ label, value }: { label: string; value: React.ReactNode }
|
|||
}
|
||||
|
||||
function DiffSection({ log }: { log: AuditLogEntry }) {
|
||||
const { t } = useTranslation();
|
||||
const { action, table_name, before_value, updated_values } = log;
|
||||
const isKeyTable = table_name === "LiteLLM_VerificationToken";
|
||||
const isUpdateAction = action === "updated" || action === "rotated";
|
||||
|
|
@ -120,8 +115,10 @@ function DiffSection({ log }: { log: AuditLogEntry }) {
|
|||
}
|
||||
});
|
||||
|
||||
displayBefore = Object.keys(changedBefore).length > 0 ? changedBefore : { note: "No differing fields detected" };
|
||||
displayAfter = Object.keys(changedAfter).length > 0 ? changedAfter : { note: "No differing fields detected" };
|
||||
displayBefore =
|
||||
Object.keys(changedBefore).length > 0 ? changedBefore : { note: t("viewLogs.auditLogDrawer.noDifferingFields") };
|
||||
displayAfter =
|
||||
Object.keys(changedAfter).length > 0 ? changedAfter : { note: t("viewLogs.auditLogDrawer.noDifferingFields") };
|
||||
}
|
||||
|
||||
const renderValue = (label: string, value: Record<string, any> | null | undefined) => {
|
||||
|
|
@ -149,17 +146,20 @@ function DiffSection({ log }: { log: AuditLogEntry }) {
|
|||
<div className="px-3 py-3 space-y-1 text-xs">
|
||||
{value.token !== undefined && (
|
||||
<p>
|
||||
<span className="text-gray-500">Token:</span> {value.token ?? "N/A"}
|
||||
<span className="text-gray-500">{t("viewLogs.auditLogDrawer.tokenLabel")}</span>{" "}
|
||||
{value.token ?? "N/A"}
|
||||
</p>
|
||||
)}
|
||||
{value.spend !== undefined && (
|
||||
<p>
|
||||
<span className="text-gray-500">Spend:</span> ${Number(value.spend).toFixed(6)}
|
||||
<span className="text-gray-500">{t("viewLogs.auditLogDrawer.spendLabel")}</span> $
|
||||
{Number(value.spend).toFixed(6)}
|
||||
</p>
|
||||
)}
|
||||
{value.max_budget !== undefined && (
|
||||
<p>
|
||||
<span className="text-gray-500">Max Budget:</span> ${Number(value.max_budget).toFixed(6)}
|
||||
<span className="text-gray-500">{t("viewLogs.auditLogDrawer.maxBudgetLabel")}</span> $
|
||||
{Number(value.max_budget).toFixed(6)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -173,13 +173,23 @@ function DiffSection({ log }: { log: AuditLogEntry }) {
|
|||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||
{renderValue("Before", displayBefore)}
|
||||
{renderValue("After", displayAfter)}
|
||||
{renderValue(t("viewLogs.auditLogDrawer.before"), displayBefore)}
|
||||
{renderValue(t("viewLogs.auditLogDrawer.after"), displayAfter)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const TABLE_NAME_DISPLAY: Record<string, string> = {
|
||||
LiteLLM_VerificationToken: t("viewLogs.auditLogDrawer.tableKeys"),
|
||||
LiteLLM_TeamTable: t("viewLogs.auditLogDrawer.tableTeams"),
|
||||
LiteLLM_UserTable: t("viewLogs.auditLogDrawer.tableUsers"),
|
||||
LiteLLM_OrganizationTable: t("viewLogs.auditLogDrawer.tableOrganizations"),
|
||||
LiteLLM_ProxyModelTable: t("viewLogs.auditLogDrawer.tableModels"),
|
||||
};
|
||||
|
||||
if (!log) return null;
|
||||
|
||||
const tableDisplay = TABLE_NAME_DISPLAY[log.table_name] ?? log.table_name;
|
||||
|
|
@ -209,7 +219,7 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) {
|
|||
<button
|
||||
onClick={onClose}
|
||||
className="w-8 h-8 flex items-center justify-center rounded hover:bg-gray-100 text-gray-500"
|
||||
aria-label="Close"
|
||||
aria-label={t("common.close")}
|
||||
>
|
||||
<CloseOutlined />
|
||||
</button>
|
||||
|
|
@ -219,19 +229,22 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) {
|
|||
<div className="px-6 py-5">
|
||||
{/* Metadata */}
|
||||
<div className="bg-gray-50 border rounded-lg p-4 mb-5">
|
||||
<p className="text-xs font-semibold text-gray-700 mb-2 uppercase tracking-wide">Details</p>
|
||||
<MetadataRow label="Table" value={tableDisplay} />
|
||||
<p className="text-xs font-semibold text-gray-700 mb-2 uppercase tracking-wide">{t("common.details")}</p>
|
||||
<MetadataRow label={t("viewLogs.auditLogDrawer.labelTable")} value={tableDisplay} />
|
||||
<MetadataRow
|
||||
label="Object ID"
|
||||
label={t("viewLogs.auditLogDrawer.labelObjectId")}
|
||||
value={
|
||||
<Text copyable className="font-mono text-xs">
|
||||
{log.object_id}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<MetadataRow label="Changed By" value={<DefaultProxyAdminTag userId={log.changed_by} />} />
|
||||
<MetadataRow
|
||||
label="API Key (Hash)"
|
||||
label={t("viewLogs.auditLogDrawer.labelChangedBy")}
|
||||
value={<DefaultProxyAdminTag userId={log.changed_by} />}
|
||||
/>
|
||||
<MetadataRow
|
||||
label={t("viewLogs.auditLogDrawer.labelApiKeyHash")}
|
||||
value={
|
||||
log.changed_by_api_key ? (
|
||||
<Text copyable className="font-mono text-xs break-all">
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import React from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
interface ConfigInfoMessageProps {
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export const ConfigInfoMessage: React.FC<ConfigInfoMessageProps> = ({ show }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
|
|
@ -27,20 +30,22 @@ export const ConfigInfoMessage: React.FC<ConfigInfoMessageProps> = ({ show }) =>
|
|||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-blue-800">Request/Response Data Not Available</h4>
|
||||
<h4 className="text-sm font-medium text-blue-800">{t("viewLogs.configInfoMessage.title")}</h4>
|
||||
<p className="text-sm text-blue-700 mt-1">
|
||||
To view request and response details, enable prompt storage in your LiteLLM configuration by adding the
|
||||
following to your <code className="bg-blue-100 px-1 py-0.5 rounded">proxy_config.yaml</code> file, or toggle
|
||||
the setting in <strong>Admin Settings → Logging Settings</strong>.
|
||||
<Trans
|
||||
i18nKey="viewLogs.configInfoMessage.description"
|
||||
components={{
|
||||
configFile: <code className="bg-blue-100 px-1 py-0.5 rounded" />,
|
||||
settingsPath: <strong />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<pre className="mt-2 bg-white p-3 rounded border border-blue-200 text-xs font-mono overflow-auto">
|
||||
{`general_settings:
|
||||
store_model_in_db: true
|
||||
store_prompts_in_spend_logs: true`}
|
||||
</pre>
|
||||
<p className="text-xs text-blue-700 mt-2">
|
||||
Note: This will only affect new requests after the configuration change.
|
||||
</p>
|
||||
<p className="text-xs text-blue-700 mt-2">{t("viewLogs.configInfoMessage.note")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from "react";
|
||||
import { Collapse } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
|
||||
export interface CostBreakdown {
|
||||
|
|
@ -49,6 +50,7 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
|
|||
cacheReadTokens,
|
||||
cacheCreationTokens,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const isCached = cacheHit?.toLowerCase() === "true";
|
||||
const hasTokenCounts = promptTokens !== undefined || completionTokens !== undefined;
|
||||
|
||||
|
|
@ -97,12 +99,12 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
|
|||
key: "1",
|
||||
label: (
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<h3 className="text-lg font-medium text-gray-900">Cost Breakdown</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900">{t("viewLogs.costBreakdownViewer.title")}</h3>
|
||||
<div className="flex items-center space-x-2 mr-4">
|
||||
<span className="text-sm text-gray-500">Total:</span>
|
||||
<span className="text-sm text-gray-500">{t("common.total")}:</span>
|
||||
<span className="text-sm font-semibold text-gray-900">
|
||||
{formatCost(totalSpend)}
|
||||
{isCached && " (Cached)"}
|
||||
{isCached && ` (${t("viewLogs.costBreakdownViewer.cached")})`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -124,24 +126,29 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
|
|||
return (
|
||||
<>
|
||||
<div className="flex text-sm">
|
||||
<span className="text-gray-600 font-medium w-1/3">Input Cost:</span>
|
||||
<span className="text-gray-600 font-medium w-1/3">
|
||||
{t("viewLogs.costBreakdownViewer.inputCost")}:
|
||||
</span>
|
||||
<span className="text-gray-900">
|
||||
{formatCost(rawCost)}
|
||||
{rawInputTokens !== undefined && rawInputTokens !== null && (
|
||||
<span className="text-gray-500 font-normal ml-1">
|
||||
({rawInputTokens.toLocaleString()} tokens)
|
||||
({rawInputTokens.toLocaleString()} {t("viewLogs.costBreakdownViewer.tokens")})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{(costBreakdown?.cache_read_cost ?? 0) > 0 && (
|
||||
<div className="flex text-sm">
|
||||
<span className="text-gray-600 font-medium w-1/3">Cache Read Cost:</span>
|
||||
<span className="text-gray-600 font-medium w-1/3">
|
||||
{t("viewLogs.costBreakdownViewer.cacheReadCost")}:
|
||||
</span>
|
||||
<span className="text-gray-900">
|
||||
{formatCost(isCached ? 0 : costBreakdown?.cache_read_cost)}
|
||||
{(cacheReadTokens ?? 0) > 0 && (
|
||||
<span className="text-gray-500 font-normal ml-1">
|
||||
({(cacheReadTokens ?? 0).toLocaleString()} tokens)
|
||||
({(cacheReadTokens ?? 0).toLocaleString()}{" "}
|
||||
{t("viewLogs.costBreakdownViewer.tokens")})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
|
@ -149,12 +156,15 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
|
|||
)}
|
||||
{(costBreakdown?.cache_creation_cost ?? 0) > 0 && (
|
||||
<div className="flex text-sm">
|
||||
<span className="text-gray-600 font-medium w-1/3">Cache Write Cost:</span>
|
||||
<span className="text-gray-600 font-medium w-1/3">
|
||||
{t("viewLogs.costBreakdownViewer.cacheWriteCost")}:
|
||||
</span>
|
||||
<span className="text-gray-900">
|
||||
{formatCost(isCached ? 0 : costBreakdown?.cache_creation_cost)}
|
||||
{(cacheCreationTokens ?? 0) > 0 && (
|
||||
<span className="text-gray-500 font-normal ml-1">
|
||||
({(cacheCreationTokens ?? 0).toLocaleString()} tokens)
|
||||
({(cacheCreationTokens ?? 0).toLocaleString()}{" "}
|
||||
{t("viewLogs.costBreakdownViewer.tokens")})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
|
@ -165,12 +175,14 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
|
|||
}
|
||||
return (
|
||||
<div className="flex text-sm">
|
||||
<span className="text-gray-600 font-medium w-1/3">Input Cost:</span>
|
||||
<span className="text-gray-600 font-medium w-1/3">
|
||||
{t("viewLogs.costBreakdownViewer.inputCost")}:
|
||||
</span>
|
||||
<span className="text-gray-900">
|
||||
{formatCost(inputCost)}
|
||||
{promptTokens !== undefined && (
|
||||
<span className="text-gray-500 font-normal ml-1">
|
||||
({promptTokens.toLocaleString()} prompt tokens)
|
||||
({promptTokens.toLocaleString()} {t("viewLogs.costBreakdownViewer.promptTokens")})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
|
@ -178,19 +190,23 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
|
|||
);
|
||||
})()}
|
||||
<div className="flex text-sm">
|
||||
<span className="text-gray-600 font-medium w-1/3">Output Cost:</span>
|
||||
<span className="text-gray-600 font-medium w-1/3">
|
||||
{t("viewLogs.costBreakdownViewer.outputCost")}:
|
||||
</span>
|
||||
<span className="text-gray-900">
|
||||
{formatCost(outputCost)}
|
||||
{completionTokens !== undefined && (
|
||||
<span className="text-gray-500 font-normal ml-1">
|
||||
({completionTokens.toLocaleString()} completion tokens)
|
||||
({completionTokens.toLocaleString()} {t("viewLogs.costBreakdownViewer.completionTokens")})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{costBreakdown?.tool_usage_cost !== undefined && costBreakdown.tool_usage_cost > 0 && (
|
||||
<div className="flex text-sm">
|
||||
<span className="text-gray-600 font-medium w-1/3">Tool Usage Cost:</span>
|
||||
<span className="text-gray-600 font-medium w-1/3">
|
||||
{t("viewLogs.costBreakdownViewer.toolUsageCost")}:
|
||||
</span>
|
||||
<span className="text-gray-900">{formatCost(costBreakdown.tool_usage_cost)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -209,7 +225,7 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
|
|||
{!isCached && (
|
||||
<div className="pt-2 border-t border-gray-100 max-w-2xl">
|
||||
<div className="flex text-sm font-semibold">
|
||||
<span className="text-gray-900 w-1/3">Original LLM Cost:</span>
|
||||
<span className="text-gray-900 w-1/3">{t("viewLogs.costBreakdownViewer.originalLlmCost")}:</span>
|
||||
<span className="text-gray-900">{formatCost(originalCost)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -224,7 +240,8 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
|
|||
{costBreakdown.discount_percent !== undefined && costBreakdown.discount_percent !== 0 && (
|
||||
<div className="flex text-sm text-gray-600">
|
||||
<span className="font-medium w-1/3">
|
||||
Discount ({formatPercent(costBreakdown.discount_percent)}):
|
||||
{t("viewLogs.costBreakdownViewer.discount")} (
|
||||
{formatPercent(costBreakdown.discount_percent)}):
|
||||
</span>
|
||||
<span className="text-gray-900">-{formatCost(costBreakdown.discount_amount)}</span>
|
||||
</div>
|
||||
|
|
@ -232,7 +249,9 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
|
|||
{costBreakdown.discount_amount !== undefined &&
|
||||
costBreakdown.discount_percent === undefined && (
|
||||
<div className="flex text-sm text-gray-600">
|
||||
<span className="font-medium w-1/3">Discount Amount:</span>
|
||||
<span className="font-medium w-1/3">
|
||||
{t("viewLogs.costBreakdownViewer.discountAmount")}:
|
||||
</span>
|
||||
<span className="text-gray-900">-{formatCost(costBreakdown.discount_amount)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -245,7 +264,8 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
|
|||
{costBreakdown.margin_percent !== undefined && costBreakdown.margin_percent !== 0 && (
|
||||
<div className="flex text-sm text-gray-600">
|
||||
<span className="font-medium w-1/3">
|
||||
Margin ({formatPercent(costBreakdown.margin_percent)}):
|
||||
{t("viewLogs.costBreakdownViewer.margin")} ({formatPercent(costBreakdown.margin_percent)}
|
||||
):
|
||||
</span>
|
||||
<span className="text-gray-900">
|
||||
+
|
||||
|
|
@ -257,7 +277,7 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
|
|||
)}
|
||||
{costBreakdown.margin_fixed_amount !== undefined && costBreakdown.margin_fixed_amount !== 0 && (
|
||||
<div className="flex text-sm text-gray-600">
|
||||
<span className="font-medium w-1/3">Margin:</span>
|
||||
<span className="font-medium w-1/3">{t("viewLogs.costBreakdownViewer.margin")}:</span>
|
||||
<span className="text-gray-900">+{formatCost(costBreakdown.margin_fixed_amount)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -269,10 +289,12 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
|
|||
{/* Final Summary */}
|
||||
<div className="mt-4 pt-4 border-t border-gray-200 max-w-2xl">
|
||||
<div className="flex items-center">
|
||||
<span className="font-bold text-sm text-gray-900 w-1/3">Final Calculated Cost:</span>
|
||||
<span className="font-bold text-sm text-gray-900 w-1/3">
|
||||
{t("viewLogs.costBreakdownViewer.finalCalculatedCost")}:
|
||||
</span>
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{formatCost(totalCost)}
|
||||
{isCached && " (Cached)"}
|
||||
{isCached && ` (${t("viewLogs.costBreakdownViewer.cached")})`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ErrorViewerProps {
|
||||
errorInfo: {
|
||||
|
|
@ -11,6 +12,7 @@ interface ErrorViewerProps {
|
|||
}
|
||||
|
||||
export const ErrorViewer: React.FC<ErrorViewerProps> = ({ errorInfo }) => {
|
||||
const { t } = useTranslation();
|
||||
const [expandedFrames, setExpandedFrames] = React.useState<{ [key: number]: boolean }>({});
|
||||
const [allExpanded, setAllExpanded] = React.useState(false);
|
||||
|
||||
|
|
@ -89,20 +91,20 @@ export const ErrorViewer: React.FC<ErrorViewerProps> = ({ errorInfo }) => {
|
|||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
Error Details
|
||||
{t("viewLogs.errorViewer.title")}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="bg-red-50 rounded-md p-4 mb-4">
|
||||
<div className="flex">
|
||||
<span className="text-red-800 font-medium w-20">Type:</span>
|
||||
<span className="text-red-700">{errorInfo.error_class || "Unknown Error"}</span>
|
||||
<span className="text-red-800 font-medium w-20">{t("viewLogs.errorViewer.type")}:</span>
|
||||
<span className="text-red-700">{errorInfo.error_class || t("viewLogs.errorViewer.unknownError")}</span>
|
||||
</div>
|
||||
<div className="flex mt-2">
|
||||
<span className="text-red-800 font-medium w-20 flex-shrink-0">Message:</span>
|
||||
<span className="text-red-800 font-medium w-20 flex-shrink-0">{t("viewLogs.errorViewer.message")}:</span>
|
||||
<span className="text-red-700 break-words whitespace-pre-wrap">
|
||||
{errorInfo.error_message || "Unknown error occurred"}
|
||||
{errorInfo.error_message || t("viewLogs.errorViewer.unknownErrorOccurred")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -110,18 +112,18 @@ export const ErrorViewer: React.FC<ErrorViewerProps> = ({ errorInfo }) => {
|
|||
{errorInfo.traceback && (
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h4 className="font-medium">Traceback</h4>
|
||||
<h4 className="font-medium">{t("viewLogs.errorViewer.traceback")}</h4>
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={toggleAllFrames}
|
||||
className="text-gray-500 hover:text-gray-700 flex items-center text-sm"
|
||||
>
|
||||
{allExpanded ? "Collapse All" : "Expand All"}
|
||||
{allExpanded ? t("viewLogs.errorViewer.collapseAll") : t("viewLogs.errorViewer.expandAll")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(errorInfo.traceback || "")}
|
||||
className="text-gray-500 hover:text-gray-700 flex items-center"
|
||||
title="Copy traceback"
|
||||
title={t("viewLogs.errorViewer.copyTraceback")}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -137,7 +139,7 @@ export const ErrorViewer: React.FC<ErrorViewerProps> = ({ errorInfo }) => {
|
|||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
<span className="ml-1">Copy</span>
|
||||
<span className="ml-1">{t("common.copy")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from "react";
|
||||
import { Card, Tag, Table, Typography, Space, Tooltip } from "antd";
|
||||
import { CheckCircleOutlined, CloseCircleOutlined, ExperimentOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ interface EvalViewerProps {
|
|||
}
|
||||
|
||||
export default function EvalViewer({ data }: EvalViewerProps) {
|
||||
const { t } = useTranslation();
|
||||
const entries: EvalInformation[] = Array.isArray(data) ? data : [data];
|
||||
|
||||
if (!entries.length) return null;
|
||||
|
|
@ -38,7 +40,7 @@ export default function EvalViewer({ data }: EvalViewerProps) {
|
|||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
|
||||
<ExperimentOutlined style={{ fontSize: 16, color: "#6366f1" }} />
|
||||
<Text strong style={{ fontSize: 15 }}>
|
||||
LLM Judge Results
|
||||
{t("viewLogs.evalViewer.title")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
|
|
@ -50,6 +52,7 @@ export default function EvalViewer({ data }: EvalViewerProps) {
|
|||
}
|
||||
|
||||
function EvalEntryCard({ entry }: { entry: EvalInformation }) {
|
||||
const { t } = useTranslation();
|
||||
const passed = entry.passed;
|
||||
const scoreColor = passed ? "#52c41a" : "#ff4d4f";
|
||||
|
||||
|
|
@ -58,7 +61,7 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
|
|||
|
||||
const columns = [
|
||||
{
|
||||
title: "Criterion",
|
||||
title: t("viewLogs.evalViewer.colCriterion"),
|
||||
dataIndex: "criterion_name",
|
||||
key: "criterion_name",
|
||||
width: 160,
|
||||
|
|
@ -69,7 +72,7 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
|
|||
),
|
||||
},
|
||||
{
|
||||
title: "Weight",
|
||||
title: t("viewLogs.evalViewer.colWeight"),
|
||||
dataIndex: "weight",
|
||||
key: "weight",
|
||||
width: 65,
|
||||
|
|
@ -81,7 +84,7 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
|
|||
) : null,
|
||||
},
|
||||
{
|
||||
title: "Score",
|
||||
title: t("viewLogs.evalViewer.colScore"),
|
||||
dataIndex: "score",
|
||||
key: "score",
|
||||
width: 65,
|
||||
|
|
@ -91,8 +94,10 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
|
|||
},
|
||||
{
|
||||
title: (
|
||||
<Tooltip title="Score × Weight — how much each criterion contributes to the final score">
|
||||
<span style={{ borderBottom: "1px dashed #aaa", cursor: "help" }}>Weighted</span>
|
||||
<Tooltip title={t("viewLogs.evalViewer.weightedTooltip")}>
|
||||
<span style={{ borderBottom: "1px dashed #aaa", cursor: "help" }}>
|
||||
{t("viewLogs.evalViewer.colWeighted")}
|
||||
</span>
|
||||
</Tooltip>
|
||||
),
|
||||
key: "weighted",
|
||||
|
|
@ -108,7 +113,7 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
|
|||
},
|
||||
},
|
||||
{
|
||||
title: "Comment",
|
||||
title: t("viewLogs.evalViewer.colComment"),
|
||||
dataIndex: "reasoning",
|
||||
key: "reasoning",
|
||||
ellipsis: { showTitle: false },
|
||||
|
|
@ -133,13 +138,13 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
|
|||
<CloseCircleOutlined style={{ color: "#ff4d4f" }} />
|
||||
)}
|
||||
<Text strong>{entry.eval_name}</Text>
|
||||
<Tag color={passed ? "success" : "error"}>{passed ? "PASSED" : "FAILED"}</Tag>
|
||||
<Tooltip
|
||||
title={`Weighted average of all criterion scores. Each criterion has a weight (%) set when the eval was created — higher-weight criteria count more toward the final score.`}
|
||||
>
|
||||
<Tag color={passed ? "success" : "error"}>
|
||||
{passed ? t("viewLogs.evalViewer.passed") : t("viewLogs.evalViewer.failed")}
|
||||
</Tag>
|
||||
<Tooltip title={t("viewLogs.evalViewer.overallScoreTooltip")}>
|
||||
<Text type="secondary" style={{ fontSize: 12, cursor: "help", borderBottom: "1px dashed #aaa" }}>
|
||||
{entry.overall_score?.toFixed(0)} / 100
|
||||
{entry.threshold != null && ` (threshold: ${entry.threshold})`}
|
||||
{entry.threshold != null && ` (${t("viewLogs.evalViewer.threshold")}: ${entry.threshold})`}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
|
@ -148,12 +153,12 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
|
|||
<Space size="small">
|
||||
{entry.judge_model && (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
Judge: {entry.judge_model}
|
||||
{t("viewLogs.evalViewer.judge")}: {entry.judge_model}
|
||||
</Text>
|
||||
)}
|
||||
{entry.iteration != null && (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
Iter: {entry.iteration + 1}
|
||||
{t("viewLogs.evalViewer.iter")}: {entry.iteration + 1}
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
|
|
@ -161,7 +166,7 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
|
|||
>
|
||||
{entry.eval_error && (
|
||||
<Text type="warning" style={{ display: "block", marginBottom: 8, fontSize: 12 }}>
|
||||
Judge error: {entry.eval_error}
|
||||
{t("viewLogs.evalViewer.judgeError")}: {entry.eval_error}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
|
|
@ -181,7 +186,7 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
|
|||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell index={0}>
|
||||
<Text strong style={{ fontSize: 12 }}>
|
||||
Total
|
||||
{t("common.total")}
|
||||
</Text>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={1} />
|
||||
|
|
@ -198,7 +203,7 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
|
|||
/>
|
||||
) : (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
Score: {entry.overall_score?.toFixed(1)} — no per-criterion breakdown available.
|
||||
{t("viewLogs.evalViewer.scoreNoCriterion", { score: entry.overall_score?.toFixed(1) })}
|
||||
</Text>
|
||||
)}
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type BedrockGuardrailAction = "NONE" | "GUARDRAIL_INTERVENED";
|
||||
|
||||
|
|
@ -114,7 +115,10 @@ const chip = (text: React.ReactNode, tone: ChipTone = "slate") => {
|
|||
return <span className={`px-2 py-1 rounded-md text-xs font-medium inline-block ${map[tone]}`}>{text}</span>;
|
||||
};
|
||||
|
||||
const boolPill = (b?: boolean) => (b ? chip("detected", "red") : chip("not detected", "slate"));
|
||||
const boolPill = (b: boolean | undefined, t: (key: string) => string) =>
|
||||
b
|
||||
? chip(t("viewLogs.bedrockGuardrailDetails.detected"), "red")
|
||||
: chip(t("viewLogs.bedrockGuardrailDetails.notDetected"), "slate");
|
||||
|
||||
interface SectionProps {
|
||||
title: string;
|
||||
|
|
@ -169,6 +173,8 @@ const Divider: React.FC = () => <div className="my-3 border-t" />;
|
|||
|
||||
/** ====== Main component ====== */
|
||||
export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailResponse }> = ({ response }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!response) return null;
|
||||
|
||||
const outputs: BedrockOutputContent[] = (response.outputs ?? response.output ?? []) as BedrockOutputContent[];
|
||||
|
|
@ -179,12 +185,18 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
<div className="flex flex-wrap gap-2">
|
||||
{response.guardrailCoverage?.textCharacters &&
|
||||
chip(
|
||||
`text guarded ${response.guardrailCoverage.textCharacters.guarded ?? 0}/${response.guardrailCoverage.textCharacters.total ?? 0}`,
|
||||
t("viewLogs.bedrockGuardrailDetails.textGuarded", {
|
||||
guarded: response.guardrailCoverage.textCharacters.guarded ?? 0,
|
||||
total: response.guardrailCoverage.textCharacters.total ?? 0,
|
||||
}),
|
||||
"blue",
|
||||
)}
|
||||
{response.guardrailCoverage?.images &&
|
||||
chip(
|
||||
`images guarded ${response.guardrailCoverage.images.guarded ?? 0}/${response.guardrailCoverage.images.total ?? 0}`,
|
||||
t("viewLogs.bedrockGuardrailDetails.imagesGuarded", {
|
||||
guarded: response.guardrailCoverage.images.guarded ?? 0,
|
||||
total: response.guardrailCoverage.images.total ?? 0,
|
||||
}),
|
||||
"blue",
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -208,17 +220,21 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
<div className="border rounded-lg p-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<KV label="Action:">{chip(response.action ?? "N/A", actionTone)}</KV>
|
||||
{response.actionReason && <KV label="Action Reason:">{response.actionReason}</KV>}
|
||||
<KV label={t("viewLogs.bedrockGuardrailDetails.action") + ":"}>
|
||||
{chip(response.action ?? "N/A", actionTone)}
|
||||
</KV>
|
||||
{response.actionReason && (
|
||||
<KV label={t("viewLogs.bedrockGuardrailDetails.actionReason") + ":"}>{response.actionReason}</KV>
|
||||
)}
|
||||
{response.blockedResponse && (
|
||||
<KV label="Blocked Response:">
|
||||
<KV label={t("viewLogs.bedrockGuardrailDetails.blockedResponse") + ":"}>
|
||||
<span className="italic">{response.blockedResponse}</span>
|
||||
</KV>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<KV label="Coverage:">{coverageChips}</KV>
|
||||
<KV label="Usage:">{usagePills}</KV>
|
||||
<KV label={t("viewLogs.bedrockGuardrailDetails.coverage") + ":"}>{coverageChips}</KV>
|
||||
<KV label={t("viewLogs.bedrockGuardrailDetails.usage") + ":"}>{usagePills}</KV>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -226,11 +242,13 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
{outputs.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<h4 className="font-medium mb-2">Outputs</h4>
|
||||
<h4 className="font-medium mb-2">{t("viewLogs.bedrockGuardrailDetails.outputs")}</h4>
|
||||
<div className="space-y-2">
|
||||
{outputs.map((o, i) => (
|
||||
<div key={i} className="p-3 bg-gray-50 rounded-md">
|
||||
<div className="text-sm whitespace-pre-wrap">{o.text ?? <em>(non-text output)</em>}</div>
|
||||
<div className="text-sm whitespace-pre-wrap">
|
||||
{o.text ?? <em>{t("viewLogs.bedrockGuardrailDetails.nonTextOutput")}</em>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -256,7 +274,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
return (
|
||||
<Section
|
||||
key={idx}
|
||||
title={`Assessment #${idx + 1}`}
|
||||
title={t("viewLogs.bedrockGuardrailDetails.assessmentTitle", { number: idx + 1 })}
|
||||
defaultOpen
|
||||
right={
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -269,9 +287,9 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
{/* Word policy */}
|
||||
{assess.wordPolicy && (
|
||||
<div className="mb-3">
|
||||
<h6 className="font-medium mb-2">Word Policy</h6>
|
||||
<h6 className="font-medium mb-2">{t("viewLogs.bedrockGuardrailDetails.wordPolicy")}</h6>
|
||||
{(assess.wordPolicy.customWords?.length ?? 0) > 0 && (
|
||||
<Section title="Custom Words" defaultOpen>
|
||||
<Section title={t("viewLogs.bedrockGuardrailDetails.customWords")} defaultOpen>
|
||||
<div className="space-y-2">
|
||||
{assess.wordPolicy.customWords!.map((w, i) => (
|
||||
<div key={i} className="flex justify-between items-center p-2 bg-gray-50 rounded">
|
||||
|
|
@ -279,14 +297,14 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
{chip(w.action ?? "N/A", w.detected ? "red" : "slate")}
|
||||
<span className="font-mono text-sm break-all">{w.match}</span>
|
||||
</div>
|
||||
{boolPill(w.detected)}
|
||||
{boolPill(w.detected, t)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
{(assess.wordPolicy.managedWordLists?.length ?? 0) > 0 && (
|
||||
<Section title="Managed Word Lists" defaultOpen={false}>
|
||||
<Section title={t("viewLogs.bedrockGuardrailDetails.managedWordLists")} defaultOpen={false}>
|
||||
<div className="space-y-2">
|
||||
{assess.wordPolicy.managedWordLists!.map((w, i) => (
|
||||
<div key={i} className="flex justify-between items-center p-2 bg-gray-50 rounded">
|
||||
|
|
@ -295,7 +313,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
<span className="font-mono text-sm break-all">{w.match}</span>
|
||||
{w.type && chip(w.type, "slate")}
|
||||
</div>
|
||||
{boolPill(w.detected)}
|
||||
{boolPill(w.detected, t)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -307,16 +325,16 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
{/* Content policy */}
|
||||
{assess.contentPolicy?.filters?.length ? (
|
||||
<div className="mb-3">
|
||||
<h6 className="font-medium mb-2">Content Policy</h6>
|
||||
<h6 className="font-medium mb-2">{t("viewLogs.bedrockGuardrailDetails.contentPolicy")}</h6>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left text-gray-600">
|
||||
<th className="py-1 pr-4">Type</th>
|
||||
<th className="py-1 pr-4">Action</th>
|
||||
<th className="py-1 pr-4">Detected</th>
|
||||
<th className="py-1 pr-4">Strength</th>
|
||||
<th className="py-1 pr-4">Confidence</th>
|
||||
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colType")}</th>
|
||||
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colAction")}</th>
|
||||
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colDetected")}</th>
|
||||
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colStrength")}</th>
|
||||
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colConfidence")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -324,7 +342,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
<tr key={i} className="border-t">
|
||||
<td className="py-1 pr-4">{f.type ?? "—"}</td>
|
||||
<td className="py-1 pr-4">{chip(f.action ?? "—", f.detected ? "red" : "slate")}</td>
|
||||
<td className="py-1 pr-4">{boolPill(f.detected)}</td>
|
||||
<td className="py-1 pr-4">{boolPill(f.detected, t)}</td>
|
||||
<td className="py-1 pr-4">{f.filterStrength ?? "—"}</td>
|
||||
<td className="py-1 pr-4">{f.confidence ?? "—"}</td>
|
||||
</tr>
|
||||
|
|
@ -338,16 +356,16 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
{/* Contextual grounding */}
|
||||
{assess.contextualGroundingPolicy?.filters?.length ? (
|
||||
<div className="mb-3">
|
||||
<h6 className="font-medium mb-2">Contextual Grounding</h6>
|
||||
<h6 className="font-medium mb-2">{t("viewLogs.bedrockGuardrailDetails.contextualGrounding")}</h6>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left text-gray-600">
|
||||
<th className="py-1 pr-4">Type</th>
|
||||
<th className="py-1 pr-4">Action</th>
|
||||
<th className="py-1 pr-4">Detected</th>
|
||||
<th className="py-1 pr-4">Score</th>
|
||||
<th className="py-1 pr-4">Threshold</th>
|
||||
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colType")}</th>
|
||||
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colAction")}</th>
|
||||
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colDetected")}</th>
|
||||
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colScore")}</th>
|
||||
<th className="py-1 pr-4">{t("viewLogs.bedrockGuardrailDetails.colThreshold")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -355,7 +373,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
<tr key={i} className="border-t">
|
||||
<td className="py-1 pr-4">{f.type ?? "—"}</td>
|
||||
<td className="py-1 pr-4">{chip(f.action ?? "—", f.detected ? "red" : "slate")}</td>
|
||||
<td className="py-1 pr-4">{boolPill(f.detected)}</td>
|
||||
<td className="py-1 pr-4">{boolPill(f.detected, t)}</td>
|
||||
<td className="py-1 pr-4">{f.score ?? "—"}</td>
|
||||
<td className="py-1 pr-4">{f.threshold ?? "—"}</td>
|
||||
</tr>
|
||||
|
|
@ -369,9 +387,9 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
{/* Sensitive Information */}
|
||||
{assess.sensitiveInformationPolicy && (
|
||||
<div className="mb-3">
|
||||
<h6 className="font-medium mb-2">Sensitive Information</h6>
|
||||
<h6 className="font-medium mb-2">{t("viewLogs.bedrockGuardrailDetails.sensitiveInformation")}</h6>
|
||||
{(assess.sensitiveInformationPolicy.piiEntities?.length ?? 0) > 0 && (
|
||||
<Section title="PII Entities" defaultOpen>
|
||||
<Section title={t("viewLogs.bedrockGuardrailDetails.piiEntities")} defaultOpen>
|
||||
<div className="space-y-2">
|
||||
{assess.sensitiveInformationPolicy.piiEntities!.map((p, i) => (
|
||||
<div key={i} className="flex justify-between items-center p-2 bg-gray-50 rounded">
|
||||
|
|
@ -380,14 +398,14 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
{p.type && chip(p.type, "slate")}
|
||||
<span className="font-mono text-xs break-all">{p.match}</span>
|
||||
</div>
|
||||
{boolPill(p.detected)}
|
||||
{boolPill(p.detected, t)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
{(assess.sensitiveInformationPolicy.regexes?.length ?? 0) > 0 && (
|
||||
<Section title="Custom Regexes" defaultOpen={false}>
|
||||
<Section title={t("viewLogs.bedrockGuardrailDetails.customRegexes")} defaultOpen={false}>
|
||||
<div className="space-y-2">
|
||||
{assess.sensitiveInformationPolicy.regexes!.map((r, i) => (
|
||||
<div
|
||||
|
|
@ -400,7 +418,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
<span className="font-mono text-xs break-all">{r.regex}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{boolPill(r.detected)}
|
||||
{boolPill(r.detected, t)}
|
||||
{r.match && <span className="font-mono text-xs break-all">{r.match}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -414,15 +432,15 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
{/* Topic policy */}
|
||||
{assess.topicPolicy?.topics?.length ? (
|
||||
<div className="mb-3">
|
||||
<h6 className="font-medium mb-2">Topic Policy</h6>
|
||||
<h6 className="font-medium mb-2">{t("viewLogs.bedrockGuardrailDetails.topicPolicy")}</h6>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{assess.topicPolicy.topics!.map((t, i) => (
|
||||
{assess.topicPolicy.topics!.map((topic, i) => (
|
||||
<div key={i} className="px-3 py-1.5 bg-gray-50 rounded-md text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
{chip(t.action ?? "N/A", t.detected ? "red" : "slate")}
|
||||
<span className="font-medium">{t.name ?? "topic"}</span>
|
||||
{t.type && chip(t.type, "slate")}
|
||||
{boolPill(t.detected)}
|
||||
{chip(topic.action ?? "N/A", topic.detected ? "red" : "slate")}
|
||||
<span className="font-medium">{topic.name ?? "topic"}</span>
|
||||
{topic.type && chip(topic.type, "slate")}
|
||||
{boolPill(topic.detected, t)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -432,31 +450,35 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
|
||||
{/* Invocation metrics */}
|
||||
{assess.invocationMetrics && (
|
||||
<Section title="Invocation Metrics" defaultOpen={false}>
|
||||
<Section title={t("viewLogs.bedrockGuardrailDetails.invocationMetrics")} defaultOpen={false}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<KV label="Latency (ms)">{assess.invocationMetrics.guardrailProcessingLatency ?? "—"}</KV>
|
||||
<KV label="Coverage:">
|
||||
<KV label={t("viewLogs.bedrockGuardrailDetails.latencyMs")}>
|
||||
{assess.invocationMetrics.guardrailProcessingLatency ?? "—"}
|
||||
</KV>
|
||||
<KV label={t("viewLogs.bedrockGuardrailDetails.coverage") + ":"}>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{assess.invocationMetrics.guardrailCoverage?.textCharacters &&
|
||||
chip(
|
||||
`text ${assess.invocationMetrics.guardrailCoverage.textCharacters.guarded ?? 0}/${
|
||||
assess.invocationMetrics.guardrailCoverage.textCharacters.total ?? 0
|
||||
}`,
|
||||
t("viewLogs.bedrockGuardrailDetails.textCoverage", {
|
||||
guarded: assess.invocationMetrics.guardrailCoverage.textCharacters.guarded ?? 0,
|
||||
total: assess.invocationMetrics.guardrailCoverage.textCharacters.total ?? 0,
|
||||
}),
|
||||
"blue",
|
||||
)}
|
||||
{assess.invocationMetrics.guardrailCoverage?.images &&
|
||||
chip(
|
||||
`images ${assess.invocationMetrics.guardrailCoverage.images.guarded ?? 0}/${
|
||||
assess.invocationMetrics.guardrailCoverage.images.total ?? 0
|
||||
}`,
|
||||
t("viewLogs.bedrockGuardrailDetails.imagesCoverage", {
|
||||
guarded: assess.invocationMetrics.guardrailCoverage.images.guarded ?? 0,
|
||||
total: assess.invocationMetrics.guardrailCoverage.images.total ?? 0,
|
||||
}),
|
||||
"blue",
|
||||
)}
|
||||
</div>
|
||||
</KV>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<KV label="Usage:">
|
||||
<KV label={t("viewLogs.bedrockGuardrailDetails.usage") + ":"}>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{assess.invocationMetrics.usage &&
|
||||
Object.entries(assess.invocationMetrics.usage).map(([k, v]) =>
|
||||
|
|
@ -478,7 +500,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
|
||||
{/* Automated reasoning (fallback render) */}
|
||||
{assess.automatedReasoningPolicy?.findings?.length ? (
|
||||
<Section title="Automated Reasoning Findings" defaultOpen={false}>
|
||||
<Section title={t("viewLogs.bedrockGuardrailDetails.automatedReasoningFindings")} defaultOpen={false}>
|
||||
<div className="space-y-2">
|
||||
{assess.automatedReasoningPolicy.findings!.map((f, i) => (
|
||||
<pre key={i} className="bg-gray-50 rounded p-2 text-xs overflow-x-auto">
|
||||
|
|
@ -495,7 +517,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
) : null}
|
||||
|
||||
{/* Raw JSON (for debugging / completeness) */}
|
||||
<Section title="Raw Bedrock Guardrail Response" defaultOpen={false}>
|
||||
<Section title={t("viewLogs.bedrockGuardrailDetails.rawResponse")} defaultOpen={false}>
|
||||
<pre className="bg-gray-50 rounded p-3 text-xs overflow-x-auto">{JSON.stringify(response, null, 2)}</pre>
|
||||
</Section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1950,6 +1950,112 @@
|
|||
"labelTotalSpend": "Total Spend (USD)"
|
||||
}
|
||||
},
|
||||
"viewLogs": {
|
||||
"auditLogDrawer": {
|
||||
"tableKeys": "Keys",
|
||||
"tableTeams": "Teams",
|
||||
"tableUsers": "Users",
|
||||
"tableOrganizations": "Organizations",
|
||||
"tableModels": "Models",
|
||||
"copyJson": "Copy JSON",
|
||||
"noDifferingFields": "No differing fields detected",
|
||||
"tokenLabel": "Token:",
|
||||
"spendLabel": "Spend:",
|
||||
"maxBudgetLabel": "Max Budget:",
|
||||
"before": "Before",
|
||||
"after": "After",
|
||||
"labelTable": "Table",
|
||||
"labelObjectId": "Object ID",
|
||||
"labelChangedBy": "Changed By",
|
||||
"labelApiKeyHash": "API Key (Hash)"
|
||||
},
|
||||
"configInfoMessage": {
|
||||
"title": "Request/Response Data Not Available",
|
||||
"description": "To view request and response details, enable prompt storage in your LiteLLM configuration by adding the following to your <configFile>proxy_config.yaml</configFile> file, or toggle the setting in <settingsPath>Admin Settings → Logging Settings</settingsPath>.",
|
||||
"note": "Note: This will only affect new requests after the configuration change."
|
||||
},
|
||||
"costBreakdownViewer": {
|
||||
"title": "Cost Breakdown",
|
||||
"cached": "Cached",
|
||||
"inputCost": "Input Cost",
|
||||
"outputCost": "Output Cost",
|
||||
"cacheReadCost": "Cache Read Cost",
|
||||
"cacheWriteCost": "Cache Write Cost",
|
||||
"toolUsageCost": "Tool Usage Cost",
|
||||
"tokens": "tokens",
|
||||
"promptTokens": "prompt tokens",
|
||||
"completionTokens": "completion tokens",
|
||||
"originalLlmCost": "Original LLM Cost",
|
||||
"discount": "Discount",
|
||||
"discountAmount": "Discount Amount",
|
||||
"margin": "Margin",
|
||||
"finalCalculatedCost": "Final Calculated Cost"
|
||||
},
|
||||
"errorViewer": {
|
||||
"title": "Error Details",
|
||||
"type": "Type",
|
||||
"message": "Message",
|
||||
"unknownError": "Unknown Error",
|
||||
"unknownErrorOccurred": "Unknown error occurred",
|
||||
"traceback": "Traceback",
|
||||
"collapseAll": "Collapse All",
|
||||
"expandAll": "Expand All",
|
||||
"copyTraceback": "Copy traceback"
|
||||
},
|
||||
"evalViewer": {
|
||||
"title": "LLM Judge Results",
|
||||
"colCriterion": "Criterion",
|
||||
"colWeight": "Weight",
|
||||
"colScore": "Score",
|
||||
"colWeighted": "Weighted",
|
||||
"weightedTooltip": "Score × Weight — how much each criterion contributes to the final score",
|
||||
"colComment": "Comment",
|
||||
"passed": "PASSED",
|
||||
"failed": "FAILED",
|
||||
"overallScoreTooltip": "Weighted average of all criterion scores. Each criterion has a weight (%) set when the eval was created — higher-weight criteria count more toward the final score.",
|
||||
"threshold": "threshold",
|
||||
"judge": "Judge",
|
||||
"iter": "Iter",
|
||||
"judgeError": "Judge error",
|
||||
"scoreNoCriterion": "Score: {{score}} — no per-criterion breakdown available."
|
||||
},
|
||||
"bedrockGuardrailDetails": {
|
||||
"detected": "detected",
|
||||
"notDetected": "not detected",
|
||||
"textGuarded": "text guarded {{guarded}}/{{total}}",
|
||||
"imagesGuarded": "images guarded {{guarded}}/{{total}}",
|
||||
"action": "Action",
|
||||
"actionReason": "Action Reason",
|
||||
"blockedResponse": "Blocked Response",
|
||||
"coverage": "Coverage",
|
||||
"usage": "Usage",
|
||||
"outputs": "Outputs",
|
||||
"nonTextOutput": "(non-text output)",
|
||||
"assessmentTitle": "Assessment #{{number}}",
|
||||
"wordPolicy": "Word Policy",
|
||||
"customWords": "Custom Words",
|
||||
"managedWordLists": "Managed Word Lists",
|
||||
"contentPolicy": "Content Policy",
|
||||
"colType": "Type",
|
||||
"colAction": "Action",
|
||||
"colDetected": "Detected",
|
||||
"colStrength": "Strength",
|
||||
"colConfidence": "Confidence",
|
||||
"contextualGrounding": "Contextual Grounding",
|
||||
"colScore": "Score",
|
||||
"colThreshold": "Threshold",
|
||||
"sensitiveInformation": "Sensitive Information",
|
||||
"piiEntities": "PII Entities",
|
||||
"customRegexes": "Custom Regexes",
|
||||
"topicPolicy": "Topic Policy",
|
||||
"invocationMetrics": "Invocation Metrics",
|
||||
"latencyMs": "Latency (ms)",
|
||||
"textCoverage": "text {{guarded}}/{{total}}",
|
||||
"imagesCoverage": "images {{guarded}}/{{total}}",
|
||||
"automatedReasoningFindings": "Automated Reasoning Findings",
|
||||
"rawResponse": "Raw Bedrock Guardrail Response"
|
||||
}
|
||||
},
|
||||
"userAgentActivity": {
|
||||
"summaryTitle": "Summary by User Agent",
|
||||
"summarySubtitle": "Performance metrics for different user agents",
|
||||
|
|
|
|||
|
|
@ -1950,6 +1950,112 @@
|
|||
"labelTotalSpend": "总花费(USD)"
|
||||
}
|
||||
},
|
||||
"viewLogs": {
|
||||
"auditLogDrawer": {
|
||||
"tableKeys": "密钥",
|
||||
"tableTeams": "团队",
|
||||
"tableUsers": "用户",
|
||||
"tableOrganizations": "组织",
|
||||
"tableModels": "模型",
|
||||
"copyJson": "复制 JSON",
|
||||
"noDifferingFields": "未检测到差异字段",
|
||||
"tokenLabel": "Token:",
|
||||
"spendLabel": "花费:",
|
||||
"maxBudgetLabel": "最大预算:",
|
||||
"before": "变更前",
|
||||
"after": "变更后",
|
||||
"labelTable": "表",
|
||||
"labelObjectId": "对象 ID",
|
||||
"labelChangedBy": "操作人",
|
||||
"labelApiKeyHash": "API 密钥(哈希)"
|
||||
},
|
||||
"configInfoMessage": {
|
||||
"title": "请求/响应数据不可用",
|
||||
"description": "要查看请求和响应详情,请在您的 LiteLLM 配置中启用提示词存储,方法是在 <configFile>proxy_config.yaml</configFile> 文件中添加以下内容,或在<settingsPath>管理员设置 → 日志设置</settingsPath>中切换该选项。",
|
||||
"note": "注意:此设置仅对配置更改后的新请求生效。"
|
||||
},
|
||||
"costBreakdownViewer": {
|
||||
"title": "费用明细",
|
||||
"cached": "已缓存",
|
||||
"inputCost": "输入费用",
|
||||
"outputCost": "输出费用",
|
||||
"cacheReadCost": "缓存读取费用",
|
||||
"cacheWriteCost": "缓存写入费用",
|
||||
"toolUsageCost": "工具使用费用",
|
||||
"tokens": "Token",
|
||||
"promptTokens": "提示词 Token",
|
||||
"completionTokens": "补全 Token",
|
||||
"originalLlmCost": "原始 LLM 费用",
|
||||
"discount": "折扣",
|
||||
"discountAmount": "折扣金额",
|
||||
"margin": "利润",
|
||||
"finalCalculatedCost": "最终计算费用"
|
||||
},
|
||||
"errorViewer": {
|
||||
"title": "错误详情",
|
||||
"type": "类型",
|
||||
"message": "消息",
|
||||
"unknownError": "未知错误",
|
||||
"unknownErrorOccurred": "发生了未知错误",
|
||||
"traceback": "堆栈跟踪",
|
||||
"collapseAll": "全部折叠",
|
||||
"expandAll": "全部展开",
|
||||
"copyTraceback": "复制堆栈跟踪"
|
||||
},
|
||||
"evalViewer": {
|
||||
"title": "LLM 评判结果",
|
||||
"colCriterion": "标准",
|
||||
"colWeight": "权重",
|
||||
"colScore": "得分",
|
||||
"colWeighted": "加权得分",
|
||||
"weightedTooltip": "得分 × 权重 — 每个标准对最终得分的贡献度",
|
||||
"colComment": "备注",
|
||||
"passed": "通过",
|
||||
"failed": "未通过",
|
||||
"overallScoreTooltip": "所有标准得分的加权平均值。每个标准在创建评估时设置权重(%),权重越高的标准对最终得分影响越大。",
|
||||
"threshold": "阈值",
|
||||
"judge": "评判模型",
|
||||
"iter": "迭代",
|
||||
"judgeError": "评判错误",
|
||||
"scoreNoCriterion": "得分:{{score}} — 无逐标准明细。"
|
||||
},
|
||||
"bedrockGuardrailDetails": {
|
||||
"detected": "已检测",
|
||||
"notDetected": "未检测",
|
||||
"textGuarded": "文本已守护 {{guarded}}/{{total}}",
|
||||
"imagesGuarded": "图片已守护 {{guarded}}/{{total}}",
|
||||
"action": "动作",
|
||||
"actionReason": "动作原因",
|
||||
"blockedResponse": "已拦截响应",
|
||||
"coverage": "覆盖范围",
|
||||
"usage": "用量",
|
||||
"outputs": "输出",
|
||||
"nonTextOutput": "(非文本输出)",
|
||||
"assessmentTitle": "评估 #{{number}}",
|
||||
"wordPolicy": "词语策略",
|
||||
"customWords": "自定义词语",
|
||||
"managedWordLists": "托管词语列表",
|
||||
"contentPolicy": "内容策略",
|
||||
"colType": "类型",
|
||||
"colAction": "动作",
|
||||
"colDetected": "是否检测到",
|
||||
"colStrength": "强度",
|
||||
"colConfidence": "置信度",
|
||||
"contextualGrounding": "上下文接地",
|
||||
"colScore": "得分",
|
||||
"colThreshold": "阈值",
|
||||
"sensitiveInformation": "敏感信息",
|
||||
"piiEntities": "PII 实体",
|
||||
"customRegexes": "自定义正则",
|
||||
"topicPolicy": "主题策略",
|
||||
"invocationMetrics": "调用指标",
|
||||
"latencyMs": "延迟(毫秒)",
|
||||
"textCoverage": "文本 {{guarded}}/{{total}}",
|
||||
"imagesCoverage": "图片 {{guarded}}/{{total}}",
|
||||
"automatedReasoningFindings": "自动推理发现",
|
||||
"rawResponse": "原始 Bedrock 护栏响应"
|
||||
}
|
||||
},
|
||||
"userAgentActivity": {
|
||||
"summaryTitle": "按 User Agent 汇总",
|
||||
"summarySubtitle": "不同 User Agent 的性能指标",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue