mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for view_logs batch 2
This commit is contained in:
parent
e6704034de
commit
1a1144fe76
10 changed files with 384 additions and 82 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Tooltip } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
checkEuAiActCompliance,
|
||||
checkGdprCompliance,
|
||||
|
|
@ -54,6 +55,7 @@ const ComplianceCard = ({
|
|||
loading: boolean;
|
||||
error: string | null;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
|
|
@ -85,12 +87,12 @@ const ComplianceCard = ({
|
|||
: "bg-red-100 text-red-700 border border-red-200"
|
||||
}`}
|
||||
>
|
||||
{data.compliant ? "COMPLIANT" : "NON-COMPLIANT"}
|
||||
{data.compliant ? t("viewLogs.compliancePanel.compliant") : t("viewLogs.compliancePanel.nonCompliant")}
|
||||
</span>
|
||||
)}
|
||||
{error && (
|
||||
<span className="px-2 py-0.5 rounded text-[11px] font-medium bg-gray-100 text-gray-500 border border-gray-200">
|
||||
UNAVAILABLE
|
||||
{t("viewLogs.compliancePanel.unavailable")}
|
||||
</span>
|
||||
)}
|
||||
<svg
|
||||
|
|
@ -107,7 +109,7 @@ const ComplianceCard = ({
|
|||
|
||||
{expanded && (
|
||||
<div className="border-t border-gray-100 px-4 py-3">
|
||||
{loading && <p className="text-sm text-gray-500">Checking compliance...</p>}
|
||||
{loading && <p className="text-sm text-gray-500">{t("viewLogs.compliancePanel.checkingCompliance")}</p>}
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
{data && (
|
||||
<div className="space-y-2">
|
||||
|
|
@ -134,6 +136,7 @@ const ComplianceCard = ({
|
|||
// -- Main Component --
|
||||
|
||||
const CompliancePanel: React.FC<CompliancePanelProps> = ({ accessToken, logEntry }) => {
|
||||
const { t } = useTranslation();
|
||||
const [euAiActData, setEuAiActData] = useState<ComplianceResponse | null>(null);
|
||||
const [gdprData, setGdprData] = useState<ComplianceResponse | null>(null);
|
||||
const [euAiActLoading, setEuAiActLoading] = useState(false);
|
||||
|
|
@ -156,20 +159,22 @@ const CompliancePanel: React.FC<CompliancePanelProps> = ({ accessToken, logEntry
|
|||
setEuAiActError(null);
|
||||
checkEuAiActCompliance(accessToken, payload)
|
||||
.then(setEuAiActData)
|
||||
.catch((err) => setEuAiActError(err.message || "Failed to check EU AI Act compliance"))
|
||||
.catch((err) => setEuAiActError(err.message || t("viewLogs.compliancePanel.euAiActCheckFailed")))
|
||||
.finally(() => setEuAiActLoading(false));
|
||||
|
||||
setGdprLoading(true);
|
||||
setGdprError(null);
|
||||
checkGdprCompliance(accessToken, payload)
|
||||
.then(setGdprData)
|
||||
.catch((err) => setGdprError(err.message || "Failed to check GDPR compliance"))
|
||||
.catch((err) => setGdprError(err.message || t("viewLogs.compliancePanel.gdprCheckFailed")))
|
||||
.finally(() => setGdprLoading(false));
|
||||
}, [accessToken, logEntry]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-4">Regulatory Compliance</h4>
|
||||
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-4">
|
||||
{t("viewLogs.compliancePanel.regulatoryCompliance")}
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
<ComplianceCard title="EU AI Act" data={euAiActData} loading={euAiActLoading} error={euAiActError} />
|
||||
<ComplianceCard title="GDPR" data={gdprData} loading={gdprLoading} error={gdprError} />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface ContentFilterDetection {
|
||||
type: "pattern" | "blocked_word" | "category_keyword";
|
||||
|
|
@ -74,13 +75,15 @@ const KV: React.FC<KVProps> = ({ label, children, mono }) => (
|
|||
);
|
||||
|
||||
const ContentFilterDetails: React.FC<ContentFilterDetailsProps> = ({ response }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Handle case where response is a string (error message) or null
|
||||
if (!response || typeof response === "string") {
|
||||
if (typeof response === "string" && response) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg border border-red-200 p-4">
|
||||
<div className="text-red-800">
|
||||
<h5 className="font-medium mb-2">Error</h5>
|
||||
<h5 className="font-medium mb-2">{t("viewLogs.contentFilterDetails.errorTitle")}</h5>
|
||||
<p className="text-sm">{response}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -95,7 +98,7 @@ const ContentFilterDetails: React.FC<ContentFilterDetailsProps> = ({ response })
|
|||
if (detections.length === 0) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<div className="text-gray-600 text-sm">No detections found</div>
|
||||
<div className="text-gray-600 text-sm">{t("viewLogs.contentFilterDetails.noDetectionsFound")}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -118,23 +121,31 @@ const ContentFilterDetails: React.FC<ContentFilterDetailsProps> = ({ response })
|
|||
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<KV label="Total Detections:">
|
||||
<KV label={t("viewLogs.contentFilterDetails.totalDetections")}>
|
||||
<span className="font-semibold">{totalDetections}</span>
|
||||
</KV>
|
||||
<KV label="Actions:">
|
||||
<KV label={t("viewLogs.contentFilterDetails.actions")}>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{blockedCount > 0 && chip(`${blockedCount} blocked`, "red")}
|
||||
{maskedCount > 0 && chip(`${maskedCount} masked`, "blue")}
|
||||
{blockedCount === 0 && maskedCount === 0 && chip("passed", "green")}
|
||||
{blockedCount > 0 &&
|
||||
chip(t("viewLogs.contentFilterDetails.blocked_other", { count: blockedCount }), "red")}
|
||||
{maskedCount > 0 &&
|
||||
chip(t("viewLogs.contentFilterDetails.masked_other", { count: maskedCount }), "blue")}
|
||||
{blockedCount === 0 && maskedCount === 0 && chip(t("viewLogs.contentFilterDetails.passed"), "green")}
|
||||
</div>
|
||||
</KV>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<KV label="By Type:">
|
||||
<KV label={t("viewLogs.contentFilterDetails.byType")}>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{patterns.length > 0 && chip(`${patterns.length} patterns`, "slate")}
|
||||
{blockedWords.length > 0 && chip(`${blockedWords.length} keywords`, "slate")}
|
||||
{categoryKeywords.length > 0 && chip(`${categoryKeywords.length} categories`, "slate")}
|
||||
{patterns.length > 0 &&
|
||||
chip(t("viewLogs.contentFilterDetails.patterns_other", { count: patterns.length }), "slate")}
|
||||
{blockedWords.length > 0 &&
|
||||
chip(t("viewLogs.contentFilterDetails.keywords_other", { count: blockedWords.length }), "slate")}
|
||||
{categoryKeywords.length > 0 &&
|
||||
chip(
|
||||
t("viewLogs.contentFilterDetails.categories_other", { count: categoryKeywords.length }),
|
||||
"slate",
|
||||
)}
|
||||
</div>
|
||||
</KV>
|
||||
</div>
|
||||
|
|
@ -143,16 +154,20 @@ const ContentFilterDetails: React.FC<ContentFilterDetailsProps> = ({ response })
|
|||
|
||||
{/* Patterns Section */}
|
||||
{patterns.length > 0 && (
|
||||
<Section title="Patterns Matched" count={patterns.length} defaultOpen={true}>
|
||||
<Section title={t("viewLogs.contentFilterDetails.patternsMatched")} count={patterns.length} defaultOpen={true}>
|
||||
<div className="space-y-2">
|
||||
{patterns.map((detection, idx) => (
|
||||
<div key={idx} className="p-3 bg-gray-50 rounded-md">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<KV label="Pattern:">{detection.pattern_name || "unknown"}</KV>
|
||||
<KV label={t("viewLogs.contentFilterDetails.patternLabel")}>
|
||||
{detection.pattern_name || t("viewLogs.contentFilterDetails.unknown")}
|
||||
</KV>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<KV label="Action:">{chip(detection.action, detection.action === "BLOCK" ? "red" : "blue")}</KV>
|
||||
<KV label={t("viewLogs.contentFilterDetails.actionLabel")}>
|
||||
{chip(detection.action, detection.action === "BLOCK" ? "red" : "blue")}
|
||||
</KV>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -163,19 +178,27 @@ const ContentFilterDetails: React.FC<ContentFilterDetailsProps> = ({ response })
|
|||
|
||||
{/* Blocked Words Section */}
|
||||
{blockedWords.length > 0 && (
|
||||
<Section title="Blocked Words Detected" count={blockedWords.length} defaultOpen={true}>
|
||||
<Section
|
||||
title={t("viewLogs.contentFilterDetails.blockedWordsDetected")}
|
||||
count={blockedWords.length}
|
||||
defaultOpen={true}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{blockedWords.map((detection, idx) => (
|
||||
<div key={idx} className="p-3 bg-gray-50 rounded-md">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<KV label="Keyword:" mono>
|
||||
{detection.keyword || "unknown"}
|
||||
<KV label={t("viewLogs.contentFilterDetails.keywordLabel")} mono>
|
||||
{detection.keyword || t("viewLogs.contentFilterDetails.unknown")}
|
||||
</KV>
|
||||
{detection.description && <KV label="Description:">{detection.description}</KV>}
|
||||
{detection.description && (
|
||||
<KV label={t("viewLogs.contentFilterDetails.descriptionLabel")}>{detection.description}</KV>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<KV label="Action:">{chip(detection.action, detection.action === "BLOCK" ? "red" : "blue")}</KV>
|
||||
<KV label={t("viewLogs.contentFilterDetails.actionLabel")}>
|
||||
{chip(detection.action, detection.action === "BLOCK" ? "red" : "blue")}
|
||||
</KV>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -186,18 +209,24 @@ const ContentFilterDetails: React.FC<ContentFilterDetailsProps> = ({ response })
|
|||
|
||||
{/* Category Keywords Section */}
|
||||
{categoryKeywords.length > 0 && (
|
||||
<Section title="Category Keywords Detected" count={categoryKeywords.length} defaultOpen={true}>
|
||||
<Section
|
||||
title={t("viewLogs.contentFilterDetails.categoryKeywordsDetected")}
|
||||
count={categoryKeywords.length}
|
||||
defaultOpen={true}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{categoryKeywords.map((detection, idx) => (
|
||||
<div key={idx} className="p-3 bg-gray-50 rounded-md">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<KV label="Category:">{detection.category || "unknown"}</KV>
|
||||
<KV label="Keyword:" mono>
|
||||
{detection.keyword || "unknown"}
|
||||
<KV label={t("viewLogs.contentFilterDetails.categoryLabel")}>
|
||||
{detection.category || t("viewLogs.contentFilterDetails.unknown")}
|
||||
</KV>
|
||||
<KV label={t("viewLogs.contentFilterDetails.keywordLabel")} mono>
|
||||
{detection.keyword || t("viewLogs.contentFilterDetails.unknown")}
|
||||
</KV>
|
||||
{detection.severity && (
|
||||
<KV label="Severity:">
|
||||
<KV label={t("viewLogs.contentFilterDetails.severityLabel")}>
|
||||
{chip(
|
||||
detection.severity,
|
||||
detection.severity === "high" ? "red" : detection.severity === "medium" ? "amber" : "slate",
|
||||
|
|
@ -206,7 +235,9 @@ const ContentFilterDetails: React.FC<ContentFilterDetailsProps> = ({ response })
|
|||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<KV label="Action:">{chip(detection.action, detection.action === "BLOCK" ? "red" : "blue")}</KV>
|
||||
<KV label={t("viewLogs.contentFilterDetails.actionLabel")}>
|
||||
{chip(detection.action, detection.action === "BLOCK" ? "red" : "blue")}
|
||||
</KV>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -216,7 +247,7 @@ const ContentFilterDetails: React.FC<ContentFilterDetailsProps> = ({ response })
|
|||
)}
|
||||
|
||||
{/* Raw JSON (for debugging) */}
|
||||
<Section title="Raw Detection Data" defaultOpen={false}>
|
||||
<Section title={t("viewLogs.contentFilterDetails.rawDetectionData")} defaultOpen={false}>
|
||||
<pre className="bg-gray-50 rounded p-3 text-xs overflow-x-auto">{JSON.stringify(detections, null, 2)}</pre>
|
||||
</Section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState, useMemo } from "react";
|
||||
import { Tooltip } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PresidioDetectedEntities from "./PresidioDetectedEntities";
|
||||
import BedrockGuardrailDetails, {
|
||||
BedrockGuardrailResponse,
|
||||
|
|
@ -250,19 +251,23 @@ const ExternalLinkIcon = () => (
|
|||
// ── Sub-components ──────────────────────────────────────────────────────────
|
||||
|
||||
const MatchDetailsTable = ({ matchDetails }: { matchDetails: MatchDetail[] }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!matchDetails || matchDetails.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
<h5 className="text-sm font-medium mb-2 text-gray-700">Match Details ({matchDetails.length})</h5>
|
||||
<h5 className="text-sm font-medium mb-2 text-gray-700">
|
||||
{t("viewLogs.guardrailViewer.matchDetails", { count: matchDetails.length })}
|
||||
</h5>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-gray-500">
|
||||
<th className="pb-2 pr-4 font-medium">Type</th>
|
||||
<th className="pb-2 pr-4 font-medium">Method</th>
|
||||
<th className="pb-2 pr-4 font-medium">Action</th>
|
||||
<th className="pb-2 font-medium">Detail</th>
|
||||
<th className="pb-2 pr-4 font-medium">{t("viewLogs.guardrailViewer.matchDetailsHeader.type")}</th>
|
||||
<th className="pb-2 pr-4 font-medium">{t("viewLogs.guardrailViewer.matchDetailsHeader.method")}</th>
|
||||
<th className="pb-2 pr-4 font-medium">{t("viewLogs.guardrailViewer.matchDetailsHeader.action")}</th>
|
||||
<th className="pb-2 font-medium">{t("viewLogs.guardrailViewer.matchDetailsHeader.detail")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -297,6 +302,7 @@ const MatchDetailsTable = ({ matchDetails }: { matchDetails: MatchDetail[] }) =>
|
|||
};
|
||||
|
||||
const GenericGuardrailResponse = ({ response }: { response: any }) => {
|
||||
const { t } = useTranslation();
|
||||
const [showRaw, setShowRaw] = useState(false);
|
||||
return (
|
||||
<div className="mt-3">
|
||||
|
|
@ -307,7 +313,7 @@ const GenericGuardrailResponse = ({ response }: { response: any }) => {
|
|||
>
|
||||
<div className="flex items-center">
|
||||
<ChevronIcon expanded={showRaw} />
|
||||
<h5 className="font-medium text-sm ml-1">Raw Guardrail Response</h5>
|
||||
<h5 className="font-medium text-sm ml-1">{t("viewLogs.guardrailViewer.rawGuardrailResponse")}</h5>
|
||||
</div>
|
||||
</div>
|
||||
{showRaw && (
|
||||
|
|
@ -331,6 +337,7 @@ interface TimelineEntry {
|
|||
}
|
||||
|
||||
const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
|
||||
const { t } = useTranslation();
|
||||
const sorted = useMemo(() => [...entries].sort((a, b) => (a.start_time ?? 0) - (b.start_time ?? 0)), [entries]);
|
||||
|
||||
const timeline = useMemo(() => {
|
||||
|
|
@ -340,7 +347,7 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
|
|||
const items: TimelineEntry[] = [];
|
||||
|
||||
// Request received
|
||||
items.push({ type: "request", label: "Request received", offsetMs: 0 });
|
||||
items.push({ type: "request", label: t("viewLogs.guardrailViewer.requestReceived"), offsetMs: 0 });
|
||||
|
||||
// Pre-call guardrails — use modeMatches so array modes (e.g. ["pre_call", "post_call"])
|
||||
// place the entry in every matching bucket.
|
||||
|
|
@ -354,9 +361,9 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
|
|||
const offsetMs = Math.round((e.end_time - baseTime) * 1000);
|
||||
items.push({
|
||||
type: "guardrail",
|
||||
label: `Pre-call guardrail: ${getDisplayName(e)}`,
|
||||
label: t("viewLogs.guardrailViewer.preCallGuardrail", { name: getDisplayName(e) }),
|
||||
offsetMs,
|
||||
status: isEntrySuccess(e) ? "PASSED" : "FAILED",
|
||||
status: isEntrySuccess(e) ? t("viewLogs.guardrailViewer.passed") : t("viewLogs.guardrailViewer.failed"),
|
||||
isSuccess: isEntrySuccess(e),
|
||||
});
|
||||
}
|
||||
|
|
@ -369,7 +376,7 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
|
|||
|
||||
items.push({
|
||||
type: "llm",
|
||||
label: "LLM call",
|
||||
label: t("viewLogs.guardrailViewer.llmCall"),
|
||||
offsetMs: llmOffsetMs,
|
||||
});
|
||||
|
||||
|
|
@ -378,9 +385,9 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
|
|||
const offsetMs = Math.round((e.end_time - baseTime) * 1000);
|
||||
items.push({
|
||||
type: "guardrail",
|
||||
label: `During-call guardrail: ${getDisplayName(e)}`,
|
||||
label: t("viewLogs.guardrailViewer.duringCallGuardrail", { name: getDisplayName(e) }),
|
||||
offsetMs,
|
||||
status: isEntrySuccess(e) ? "PASSED" : "FAILED",
|
||||
status: isEntrySuccess(e) ? t("viewLogs.guardrailViewer.passed") : t("viewLogs.guardrailViewer.failed"),
|
||||
isSuccess: isEntrySuccess(e),
|
||||
});
|
||||
}
|
||||
|
|
@ -390,9 +397,9 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
|
|||
const offsetMs = Math.round((e.end_time - baseTime) * 1000);
|
||||
items.push({
|
||||
type: "guardrail",
|
||||
label: `Post-call guardrail: ${getDisplayName(e)}`,
|
||||
label: t("viewLogs.guardrailViewer.postCallGuardrail", { name: getDisplayName(e) }),
|
||||
offsetMs,
|
||||
status: isEntrySuccess(e) ? "PASSED" : "FAILED",
|
||||
status: isEntrySuccess(e) ? t("viewLogs.guardrailViewer.passed") : t("viewLogs.guardrailViewer.failed"),
|
||||
isSuccess: isEntrySuccess(e),
|
||||
});
|
||||
}
|
||||
|
|
@ -400,14 +407,16 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
|
|||
// Response returned
|
||||
const maxEnd = Math.max(...sorted.map((e) => e.end_time));
|
||||
const responseOffsetMs = Math.round((maxEnd - baseTime) * 1000) + 1;
|
||||
items.push({ type: "response", label: "Response returned", offsetMs: responseOffsetMs });
|
||||
items.push({ type: "response", label: t("viewLogs.guardrailViewer.responseReturned"), offsetMs: responseOffsetMs });
|
||||
|
||||
return items;
|
||||
}, [sorted]);
|
||||
}, [sorted, t]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-4">Request Lifecycle</h4>
|
||||
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-4">
|
||||
{t("viewLogs.guardrailViewer.requestLifecycle")}
|
||||
</h4>
|
||||
<div className="relative">
|
||||
{timeline.map((item, idx) => (
|
||||
<div key={idx} className="flex items-start gap-3 relative">
|
||||
|
|
@ -457,6 +466,7 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
|
|||
// ── Evaluation Card ─────────────────────────────────────────────────────────
|
||||
|
||||
const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => {
|
||||
const { t } = useTranslation();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const success = isEntrySuccess(entry);
|
||||
const totalMasked = getTotalMasked(entry);
|
||||
|
|
@ -509,7 +519,7 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => {
|
|||
: "bg-red-100 text-red-700 border border-red-200"
|
||||
}`}
|
||||
>
|
||||
{success ? "PASSED" : "FAILED"}
|
||||
{success ? t("viewLogs.guardrailViewer.passed") : t("viewLogs.guardrailViewer.failed")}
|
||||
</span>
|
||||
|
||||
{matchCountStr && (
|
||||
|
|
@ -526,16 +536,18 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => {
|
|||
|
||||
{entry.confidence_score != null && (
|
||||
<span className="px-2 py-0.5 bg-gray-100 text-gray-600 border border-gray-200 rounded text-[11px] font-medium flex-shrink-0">
|
||||
{(entry.confidence_score * 100).toFixed(0)}% conf
|
||||
{t("viewLogs.guardrailViewer.confidencePercent", {
|
||||
percent: (entry.confidence_score * 100).toFixed(0),
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{riskScore != null && success && (
|
||||
<Tooltip title={`Risk score: ${riskScore}/10`}>
|
||||
<Tooltip title={t("viewLogs.guardrailViewer.riskScoreTooltip", { score: riskScore })}>
|
||||
<span
|
||||
className={`px-2 py-0.5 border rounded text-[11px] font-semibold flex-shrink-0 ${getRiskColor(riskScore)}`}
|
||||
>
|
||||
Risk {riskScore}/10
|
||||
{t("viewLogs.guardrailViewer.riskScore", { score: riskScore })}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
|
@ -559,28 +571,36 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => {
|
|||
{/* Classification details for llm-judge */}
|
||||
{entry.classification && (
|
||||
<div className="mb-3 bg-gray-50 rounded-lg p-3 space-y-1">
|
||||
<h5 className="text-sm font-medium text-gray-700 mb-2">Classification</h5>
|
||||
<h5 className="text-sm font-medium text-gray-700 mb-2">{t("viewLogs.guardrailViewer.classification")}</h5>
|
||||
{entry.classification.category && (
|
||||
<div className="flex text-sm">
|
||||
<span className="font-medium w-1/3 text-gray-500">Category:</span>
|
||||
<span className="font-medium w-1/3 text-gray-500">
|
||||
{t("viewLogs.guardrailViewer.classificationCategory")}
|
||||
</span>
|
||||
<span>{entry.classification.category}</span>
|
||||
</div>
|
||||
)}
|
||||
{entry.classification.article_reference && (
|
||||
<div className="flex text-sm">
|
||||
<span className="font-medium w-1/3 text-gray-500">Reference:</span>
|
||||
<span className="font-medium w-1/3 text-gray-500">
|
||||
{t("viewLogs.guardrailViewer.classificationReference")}
|
||||
</span>
|
||||
<span className="font-mono">{entry.classification.article_reference}</span>
|
||||
</div>
|
||||
)}
|
||||
{entry.classification.confidence != null && (
|
||||
<div className="flex text-sm">
|
||||
<span className="font-medium w-1/3 text-gray-500">Confidence:</span>
|
||||
<span className="font-medium w-1/3 text-gray-500">
|
||||
{t("viewLogs.guardrailViewer.classificationConfidence")}
|
||||
</span>
|
||||
<span>{(entry.classification.confidence * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
)}
|
||||
{entry.classification.reason && (
|
||||
<div className="flex text-sm">
|
||||
<span className="font-medium w-1/3 text-gray-500">Reason:</span>
|
||||
<span className="font-medium w-1/3 text-gray-500">
|
||||
{t("viewLogs.guardrailViewer.classificationReason")}
|
||||
</span>
|
||||
<span>{entry.classification.reason}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -595,7 +615,7 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => {
|
|||
{/* Masked entity summary */}
|
||||
{totalMasked > 0 && (
|
||||
<div className="mt-3">
|
||||
<h5 className="text-sm font-medium text-gray-700 mb-2">Masked Entities</h5>
|
||||
<h5 className="text-sm font-medium text-gray-700 mb-2">{t("viewLogs.guardrailViewer.maskedEntities")}</h5>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries(entry.masked_entity_count || {}).map(([entityType, count]) => (
|
||||
<span key={entityType} className="px-2 py-1 bg-blue-50 text-blue-700 rounded text-xs font-medium">
|
||||
|
|
@ -634,6 +654,7 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => {
|
|||
// ── Main Component ──────────────────────────────────────────────────────────
|
||||
|
||||
const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps) => {
|
||||
const { t } = useTranslation();
|
||||
const guardrailEntries = useMemo(() => {
|
||||
return Array.isArray(data)
|
||||
? data.filter((entry): entry is GuardrailInformation => Boolean(entry))
|
||||
|
|
@ -676,10 +697,10 @@ const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps)
|
|||
<div className="flex items-center gap-4">
|
||||
<ShieldIcon />
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">Guardrails & Policy Compliance</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{t("viewLogs.guardrailViewer.headerTitle")}</h3>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className="text-sm text-gray-500">
|
||||
{guardrailEntries.length} guardrail{guardrailEntries.length !== 1 ? "s" : ""} evaluated
|
||||
{t("viewLogs.guardrailViewer.guardrailsEvaluated_other", { count: guardrailEntries.length })}
|
||||
</span>
|
||||
<span className="text-gray-300">|</span>
|
||||
<span
|
||||
|
|
@ -700,7 +721,7 @@ const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps)
|
|||
/>
|
||||
</svg>
|
||||
) : null}
|
||||
{passedCount} Passed
|
||||
{t("viewLogs.guardrailViewer.passedCount", { count: passedCount })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -708,7 +729,9 @@ const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps)
|
|||
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-medium text-gray-900">Total: {totalOverheadMs}ms overhead</div>
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{t("viewLogs.guardrailViewer.totalOverhead", { ms: totalOverheadMs })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
|
@ -716,7 +739,7 @@ const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps)
|
|||
className="inline-flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<DownloadIcon />
|
||||
Export Compliance Log
|
||||
{t("viewLogs.guardrailViewer.exportComplianceLog")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -737,7 +760,9 @@ const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps)
|
|||
|
||||
{/* Evaluation Details */}
|
||||
<div className="px-6 py-5">
|
||||
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-4">Evaluation Details</h4>
|
||||
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-4">
|
||||
{t("viewLogs.guardrailViewer.evaluationDetails")}
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{guardrailEntries.map((entry, index) => (
|
||||
<EvaluationCard key={`${entry.guardrail_name ?? "guardrail"}-${index}`} entry={entry} />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface RecognitionMetadata {
|
||||
recognizer_name: string;
|
||||
|
|
@ -24,6 +25,7 @@ const getScoreColor = (score: number): string => {
|
|||
};
|
||||
|
||||
const PresidioDetectedEntities = ({ entities }: PresidioDetectedEntitiesProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [entityListExpanded, setEntityListExpanded] = useState(true);
|
||||
const [expandedEntities, setExpandedEntities] = useState<Record<number, boolean>>({});
|
||||
|
||||
|
|
@ -47,7 +49,9 @@ const PresidioDetectedEntities = ({ entities }: PresidioDetectedEntitiesProps) =
|
|||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<h4 className="font-medium">Detected Entities ({entities.length})</h4>
|
||||
<h4 className="font-medium">
|
||||
{t("viewLogs.presidioDetectedEntities.detectedEntities", { count: entities.length })}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
{entityListExpanded && (
|
||||
|
|
@ -71,10 +75,15 @@ const PresidioDetectedEntities = ({ entities }: PresidioDetectedEntitiesProps) =
|
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<span className="font-medium mr-2">{entity.entity_type}</span>
|
||||
<span className={`font-mono ${getScoreColor(entity.score)}`}>Score: {entity.score.toFixed(2)}</span>
|
||||
<span className={`font-mono ${getScoreColor(entity.score)}`}>
|
||||
{t("viewLogs.presidioDetectedEntities.scoreLabel", { score: entity.score.toFixed(2) })}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
Position: {entity.start}-{entity.end}
|
||||
{t("viewLogs.presidioDetectedEntities.positionRange", {
|
||||
start: entity.start,
|
||||
end: entity.end,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -83,17 +92,26 @@ const PresidioDetectedEntities = ({ entities }: PresidioDetectedEntitiesProps) =
|
|||
<div className="grid grid-cols-2 gap-4 mb-2">
|
||||
<div className="space-y-2">
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Entity Type:</span>
|
||||
<span className="font-medium w-1/3">
|
||||
{t("viewLogs.presidioDetectedEntities.entityTypeLabel")}
|
||||
</span>
|
||||
<span>{entity.entity_type}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Position:</span>
|
||||
<span className="font-medium w-1/3">
|
||||
{t("viewLogs.presidioDetectedEntities.positionLabel")}
|
||||
</span>
|
||||
<span>
|
||||
Characters {entity.start}-{entity.end}
|
||||
{t("viewLogs.presidioDetectedEntities.characters", {
|
||||
start: entity.start,
|
||||
end: entity.end,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Confidence:</span>
|
||||
<span className="font-medium w-1/3">
|
||||
{t("viewLogs.presidioDetectedEntities.confidenceLabel")}
|
||||
</span>
|
||||
<span className={getScoreColor(entity.score)}>{entity.score.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -102,11 +120,15 @@ const PresidioDetectedEntities = ({ entities }: PresidioDetectedEntitiesProps) =
|
|||
{entity.recognition_metadata && (
|
||||
<>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Recognizer:</span>
|
||||
<span className="font-medium w-1/3">
|
||||
{t("viewLogs.presidioDetectedEntities.recognizerLabel")}
|
||||
</span>
|
||||
<span>{entity.recognition_metadata.recognizer_name}</span>
|
||||
</div>
|
||||
<div className="flex overflow-hidden">
|
||||
<span className="font-medium w-1/3">Identifier:</span>
|
||||
<span className="font-medium w-1/3">
|
||||
{t("viewLogs.presidioDetectedEntities.identifierLabel")}
|
||||
</span>
|
||||
<span className="truncate text-xs font-mono">
|
||||
{entity.recognition_metadata.recognizer_identifier}
|
||||
</span>
|
||||
|
|
@ -115,7 +137,9 @@ const PresidioDetectedEntities = ({ entities }: PresidioDetectedEntitiesProps) =
|
|||
)}
|
||||
{entity.analysis_explanation && (
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Explanation:</span>
|
||||
<span className="font-medium w-1/3">
|
||||
{t("viewLogs.presidioDetectedEntities.explanationLabel")}
|
||||
</span>
|
||||
<span>{entity.analysis_explanation}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { useState } from "react";
|
||||
import { Typography } from "antd";
|
||||
import { DownOutlined, RightOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ interface CollapsibleMessageProps {
|
|||
}
|
||||
|
||||
export function CollapsibleMessage({ label, content, defaultExpanded = false }: CollapsibleMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const charCount = content?.length || 0;
|
||||
|
|
@ -52,7 +54,7 @@ export function CollapsibleMessage({ label, content, defaultExpanded = false }:
|
|||
{label}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 10 }}>
|
||||
({charCount.toLocaleString()} chars)
|
||||
{t("viewLogs.collapsibleMessage.charCount", { count: charCount.toLocaleString() })}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Button, Space, Tag, Tooltip, Typography } from "antd";
|
||||
import { CloseOutlined, UpOutlined, DownOutlined } from "@ant-design/icons";
|
||||
import moment from "moment";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LogEntry } from "../columns";
|
||||
import { getProviderLogoAndName } from "../../provider_info_helpers";
|
||||
import {
|
||||
|
|
@ -40,6 +41,7 @@ export function DrawerHeader({
|
|||
statusColor,
|
||||
environment,
|
||||
}: DrawerHeaderProps) {
|
||||
const { t } = useTranslation();
|
||||
const provider = log.custom_llm_provider || "";
|
||||
const providerInfo = provider ? getProviderLogoAndName(provider) : null;
|
||||
|
||||
|
|
@ -87,12 +89,13 @@ function ModelProviderSection({
|
|||
providerLogo?: string;
|
||||
providerName?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Space size={SPACING_MEDIUM} style={{ marginBottom: SPACING_MEDIUM }}>
|
||||
{providerLogo && (
|
||||
<img
|
||||
src={providerLogo}
|
||||
alt={providerName || "Provider"}
|
||||
alt={providerName || t("viewLogs.drawerHeader.providerAlt")}
|
||||
style={{ width: 24, height: 24 }}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
|
|
@ -118,12 +121,16 @@ function ModelProviderSection({
|
|||
* Request ID display with copy functionality
|
||||
*/
|
||||
function RequestIdSection({ requestId }: { requestId: string }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<Tooltip title={requestId}>
|
||||
<Text
|
||||
strong
|
||||
copyable={{ text: requestId, tooltips: ["Copy Request ID", "Copied!"] }}
|
||||
copyable={{
|
||||
text: requestId,
|
||||
tooltips: [t("viewLogs.drawerHeader.copyRequestId"), t("viewLogs.drawerHeader.copiedRequestId")],
|
||||
}}
|
||||
style={{
|
||||
fontSize: FONT_SIZE_HEADER,
|
||||
fontFamily: FONT_FAMILY_MONO,
|
||||
|
|
@ -153,6 +160,7 @@ function NavigationSection({
|
|||
onNext: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const keyboardShortcutStyle = {
|
||||
border: "1px solid #d9d9d9",
|
||||
borderRadius: 4,
|
||||
|
|
@ -173,7 +181,7 @@ function NavigationSection({
|
|||
<DownOutlined />
|
||||
<span style={keyboardShortcutStyle}>J</span>
|
||||
</Button>
|
||||
<Tooltip title="ESC to close">
|
||||
<Tooltip title={t("viewLogs.drawerHeader.escToClose")}>
|
||||
<Button type="text" icon={<CloseOutlined />} onClick={onClose} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
|
@ -194,10 +202,11 @@ function StatusBar({
|
|||
statusColor: "error" | "success";
|
||||
environment: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Space size={SPACING_LARGE}>
|
||||
<Tag color={statusColor}>{statusLabel}</Tag>
|
||||
<Tag>Env: {environment}</Tag>
|
||||
<Tag>{t("viewLogs.drawerHeader.envTag", { env: environment })}</Tag>
|
||||
<Space size={SPACING_MEDIUM}>
|
||||
<Text type="secondary" style={{ fontSize: FONT_SIZE_MEDIUM }}>
|
||||
{moment(log.startTime).format("MMM D, YYYY h:mm:ss A")}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { useState } from "react";
|
||||
import { Typography } from "antd";
|
||||
import { DownOutlined, RightOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ParsedMessage } from "./prettyMessagesTypes";
|
||||
import { SimpleMessageBlock } from "./SimpleMessageBlock";
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ interface HistoryTreeProps {
|
|||
}
|
||||
|
||||
export function HistoryTree({ messages }: HistoryTreeProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
|
|
@ -48,7 +50,7 @@ export function HistoryTree({ messages }: HistoryTreeProps) {
|
|||
<RightOutlined style={{ fontSize: 10, color: "#8c8c8c" }} />
|
||||
)}
|
||||
<Text type="secondary" style={{ fontSize: 10, letterSpacing: "0.5px", textTransform: "uppercase" }}>
|
||||
HISTORY ({messages.length} message{messages.length !== 1 ? "s" : ""})
|
||||
{t("viewLogs.historyTree.history_other", { count: messages.length })}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Typography } from "antd";
|
||||
import { JsonView, defaultStyles } from "react-json-view-lite";
|
||||
import "react-json-view-lite/dist/index.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { JSON_MAX_HEIGHT, COLOR_BG_LIGHT, SPACING_LARGE } from "./constants";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
|
@ -15,7 +16,8 @@ interface JsonViewerProps {
|
|||
* Uses an interactive tree component for easy navigation.
|
||||
*/
|
||||
export function JsonViewer({ data }: JsonViewerProps) {
|
||||
if (!data) return <Text type="secondary">No data</Text>;
|
||||
const { t } = useTranslation();
|
||||
if (!data) return <Text type="secondary">{t("common.noData")}</Text>;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -2074,5 +2074,106 @@
|
|||
"dailyActiveUsers": "Daily Active Users - Last 7 Days",
|
||||
"weeklyActiveUsers": "Weekly Active Users - Last 7 Weeks",
|
||||
"monthlyActiveUsers": "Monthly Active Users - Last 7 Months"
|
||||
},
|
||||
"viewLogs": {
|
||||
"compliancePanel": {
|
||||
"checkingCompliance": "Checking compliance...",
|
||||
"compliant": "COMPLIANT",
|
||||
"nonCompliant": "NON-COMPLIANT",
|
||||
"unavailable": "UNAVAILABLE",
|
||||
"regulatoryCompliance": "Regulatory Compliance",
|
||||
"euAiActCheckFailed": "Failed to check EU AI Act compliance",
|
||||
"gdprCheckFailed": "Failed to check GDPR compliance"
|
||||
},
|
||||
"contentFilterDetails": {
|
||||
"errorTitle": "Error",
|
||||
"noDetectionsFound": "No detections found",
|
||||
"totalDetections": "Total Detections:",
|
||||
"actions": "Actions:",
|
||||
"byType": "By Type:",
|
||||
"patternsMatched": "Patterns Matched",
|
||||
"blockedWordsDetected": "Blocked Words Detected",
|
||||
"categoryKeywordsDetected": "Category Keywords Detected",
|
||||
"rawDetectionData": "Raw Detection Data",
|
||||
"patternLabel": "Pattern:",
|
||||
"keywordLabel": "Keyword:",
|
||||
"descriptionLabel": "Description:",
|
||||
"categoryLabel": "Category:",
|
||||
"severityLabel": "Severity:",
|
||||
"actionLabel": "Action:",
|
||||
"unknown": "unknown",
|
||||
"passed": "passed",
|
||||
"blocked_one": "{{count}} blocked",
|
||||
"blocked_other": "{{count}} blocked",
|
||||
"masked_one": "{{count}} masked",
|
||||
"masked_other": "{{count}} masked",
|
||||
"patterns_one": "{{count}} patterns",
|
||||
"patterns_other": "{{count}} patterns",
|
||||
"keywords_one": "{{count}} keywords",
|
||||
"keywords_other": "{{count}} keywords",
|
||||
"categories_one": "{{count}} categories",
|
||||
"categories_other": "{{count}} categories"
|
||||
},
|
||||
"guardrailViewer": {
|
||||
"requestLifecycle": "Request Lifecycle",
|
||||
"evaluationDetails": "Evaluation Details",
|
||||
"requestReceived": "Request received",
|
||||
"llmCall": "LLM call",
|
||||
"responseReturned": "Response returned",
|
||||
"preCallGuardrail": "Pre-call guardrail: {{name}}",
|
||||
"duringCallGuardrail": "During-call guardrail: {{name}}",
|
||||
"postCallGuardrail": "Post-call guardrail: {{name}}",
|
||||
"passed": "PASSED",
|
||||
"failed": "FAILED",
|
||||
"rawGuardrailResponse": "Raw Guardrail Response",
|
||||
"matchDetails": "Match Details ({{count}})",
|
||||
"matchDetailsHeader": {
|
||||
"type": "Type",
|
||||
"method": "Method",
|
||||
"action": "Action",
|
||||
"detail": "Detail"
|
||||
},
|
||||
"classification": "Classification",
|
||||
"classificationCategory": "Category:",
|
||||
"classificationReference": "Reference:",
|
||||
"classificationConfidence": "Confidence:",
|
||||
"classificationReason": "Reason:",
|
||||
"maskedEntities": "Masked Entities",
|
||||
"headerTitle": "Guardrails & Policy Compliance",
|
||||
"guardrailsEvaluated_one": "{{count}} guardrail evaluated",
|
||||
"guardrailsEvaluated_other": "{{count}} guardrails evaluated",
|
||||
"passedCount": "{{count}} Passed",
|
||||
"totalOverhead": "Total: {{ms}}ms overhead",
|
||||
"exportComplianceLog": "Export Compliance Log",
|
||||
"riskScore": "Risk {{score}}/10",
|
||||
"riskScoreTooltip": "Risk score: {{score}}/10",
|
||||
"confidencePercent": "{{percent}}% conf"
|
||||
},
|
||||
"presidioDetectedEntities": {
|
||||
"detectedEntities": "Detected Entities ({{count}})",
|
||||
"scoreLabel": "Score: {{score}}",
|
||||
"positionRange": "Position: {{start}}-{{end}}",
|
||||
"entityTypeLabel": "Entity Type:",
|
||||
"positionLabel": "Position:",
|
||||
"characters": "Characters {{start}}-{{end}}",
|
||||
"confidenceLabel": "Confidence:",
|
||||
"recognizerLabel": "Recognizer:",
|
||||
"identifierLabel": "Identifier:",
|
||||
"explanationLabel": "Explanation:"
|
||||
},
|
||||
"collapsibleMessage": {
|
||||
"charCount": "({{count}} chars)"
|
||||
},
|
||||
"drawerHeader": {
|
||||
"copyRequestId": "Copy Request ID",
|
||||
"copiedRequestId": "Copied!",
|
||||
"escToClose": "ESC to close",
|
||||
"envTag": "Env: {{env}}",
|
||||
"providerAlt": "Provider"
|
||||
},
|
||||
"historyTree": {
|
||||
"history_one": "HISTORY ({{count}} message)",
|
||||
"history_other": "HISTORY ({{count}} messages)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2074,5 +2074,106 @@
|
|||
"dailyActiveUsers": "每日活跃用户 - 近 7 天",
|
||||
"weeklyActiveUsers": "每周活跃用户 - 近 7 周",
|
||||
"monthlyActiveUsers": "每月活跃用户 - 近 7 个月"
|
||||
},
|
||||
"viewLogs": {
|
||||
"compliancePanel": {
|
||||
"checkingCompliance": "正在检查合规性...",
|
||||
"compliant": "合规",
|
||||
"nonCompliant": "不合规",
|
||||
"unavailable": "不可用",
|
||||
"regulatoryCompliance": "监管合规",
|
||||
"euAiActCheckFailed": "检查 EU AI Act 合规性失败",
|
||||
"gdprCheckFailed": "检查 GDPR 合规性失败"
|
||||
},
|
||||
"contentFilterDetails": {
|
||||
"errorTitle": "错误",
|
||||
"noDetectionsFound": "未发现任何检测结果",
|
||||
"totalDetections": "检测总数:",
|
||||
"actions": "操作:",
|
||||
"byType": "按类型:",
|
||||
"patternsMatched": "匹配的模式",
|
||||
"blockedWordsDetected": "检测到的屏蔽词",
|
||||
"categoryKeywordsDetected": "检测到的分类关键词",
|
||||
"rawDetectionData": "原始检测数据",
|
||||
"patternLabel": "模式:",
|
||||
"keywordLabel": "关键词:",
|
||||
"descriptionLabel": "描述:",
|
||||
"categoryLabel": "分类:",
|
||||
"severityLabel": "严重程度:",
|
||||
"actionLabel": "操作:",
|
||||
"unknown": "未知",
|
||||
"passed": "通过",
|
||||
"blocked_one": "{{count}} 个已屏蔽",
|
||||
"blocked_other": "{{count}} 个已屏蔽",
|
||||
"masked_one": "{{count}} 个已脱敏",
|
||||
"masked_other": "{{count}} 个已脱敏",
|
||||
"patterns_one": "{{count}} 个模式",
|
||||
"patterns_other": "{{count}} 个模式",
|
||||
"keywords_one": "{{count}} 个关键词",
|
||||
"keywords_other": "{{count}} 个关键词",
|
||||
"categories_one": "{{count}} 个分类",
|
||||
"categories_other": "{{count}} 个分类"
|
||||
},
|
||||
"guardrailViewer": {
|
||||
"requestLifecycle": "请求生命周期",
|
||||
"evaluationDetails": "评估详情",
|
||||
"requestReceived": "收到请求",
|
||||
"llmCall": "LLM 调用",
|
||||
"responseReturned": "返回响应",
|
||||
"preCallGuardrail": "前置护栏:{{name}}",
|
||||
"duringCallGuardrail": "调用中护栏:{{name}}",
|
||||
"postCallGuardrail": "后置护栏:{{name}}",
|
||||
"passed": "通过",
|
||||
"failed": "失败",
|
||||
"rawGuardrailResponse": "原始护栏响应",
|
||||
"matchDetails": "匹配详情({{count}})",
|
||||
"matchDetailsHeader": {
|
||||
"type": "类型",
|
||||
"method": "方法",
|
||||
"action": "操作",
|
||||
"detail": "详情"
|
||||
},
|
||||
"classification": "分类",
|
||||
"classificationCategory": "分类:",
|
||||
"classificationReference": "引用:",
|
||||
"classificationConfidence": "置信度:",
|
||||
"classificationReason": "原因:",
|
||||
"maskedEntities": "已脱敏实体",
|
||||
"headerTitle": "护栏与策略合规",
|
||||
"guardrailsEvaluated_one": "已评估 {{count}} 个护栏",
|
||||
"guardrailsEvaluated_other": "已评估 {{count}} 个护栏",
|
||||
"passedCount": "{{count}} 个通过",
|
||||
"totalOverhead": "总计:{{ms}}ms 开销",
|
||||
"exportComplianceLog": "导出合规日志",
|
||||
"riskScore": "风险 {{score}}/10",
|
||||
"riskScoreTooltip": "风险评分:{{score}}/10",
|
||||
"confidencePercent": "{{percent}}% 置信度"
|
||||
},
|
||||
"presidioDetectedEntities": {
|
||||
"detectedEntities": "检测到的实体({{count}})",
|
||||
"scoreLabel": "得分:{{score}}",
|
||||
"positionRange": "位置:{{start}}-{{end}}",
|
||||
"entityTypeLabel": "实体类型:",
|
||||
"positionLabel": "位置:",
|
||||
"characters": "字符 {{start}}-{{end}}",
|
||||
"confidenceLabel": "置信度:",
|
||||
"recognizerLabel": "识别器:",
|
||||
"identifierLabel": "标识符:",
|
||||
"explanationLabel": "说明:"
|
||||
},
|
||||
"collapsibleMessage": {
|
||||
"charCount": "({{count}} 字符)"
|
||||
},
|
||||
"drawerHeader": {
|
||||
"copyRequestId": "复制请求 ID",
|
||||
"copiedRequestId": "已复制!",
|
||||
"escToClose": "按 ESC 关闭",
|
||||
"envTag": "环境:{{env}}",
|
||||
"providerAlt": "提供商"
|
||||
},
|
||||
"historyTree": {
|
||||
"history_one": "历史({{count}} 条消息)",
|
||||
"history_other": "历史({{count}} 条消息)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue