mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for pipeline flow builder
This commit is contained in:
parent
1505d4477d
commit
aa883b580e
3 changed files with 444 additions and 219 deletions
|
|
@ -1,4 +1,6 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import { Select, Typography, Spin } from "antd";
|
||||
import MessageManager from "@/components/molecules/message_manager";
|
||||
import { Button, TextInput } from "@tremor/react";
|
||||
|
|
@ -30,19 +32,19 @@ function getPromptsForTestSource(source: string): CompliancePrompt[] {
|
|||
|
||||
const { Text } = Typography;
|
||||
|
||||
const ACTION_OPTIONS = [
|
||||
{ label: "Next Step", value: "next" },
|
||||
{ label: "Allow", value: "allow" },
|
||||
{ label: "Block", value: "block" },
|
||||
{ label: "Custom Response", value: "modify_response" },
|
||||
const getActionOptions = (t: TFunction) => [
|
||||
{ label: t("policies.pipelineFlowBuilder.actionNextStep"), value: "next" },
|
||||
{ label: t("policies.pipelineFlowBuilder.actionAllow"), value: "allow" },
|
||||
{ label: t("policies.pipelineFlowBuilder.actionBlock"), value: "block" },
|
||||
{ label: t("policies.pipelineFlowBuilder.actionCustomResponse"), value: "modify_response" },
|
||||
];
|
||||
|
||||
const ACTION_LABELS: Record<string, string> = {
|
||||
allow: "Allow",
|
||||
block: "Block",
|
||||
next: "Next Step",
|
||||
modify_response: "Custom Response",
|
||||
};
|
||||
const getActionLabels = (t: TFunction): Record<string, string> => ({
|
||||
allow: t("policies.pipelineFlowBuilder.actionAllow"),
|
||||
block: t("policies.pipelineFlowBuilder.actionBlock"),
|
||||
next: t("policies.pipelineFlowBuilder.actionNextStep"),
|
||||
modify_response: t("policies.pipelineFlowBuilder.actionCustomResponse"),
|
||||
});
|
||||
|
||||
function createDefaultStep(): PipelineStep {
|
||||
return {
|
||||
|
|
@ -210,37 +212,40 @@ interface ConnectorProps {
|
|||
onInsert: () => void;
|
||||
}
|
||||
|
||||
const Connector: React.FC<ConnectorProps> = ({ onInsert }) => (
|
||||
<div className="flex flex-col items-center" style={{ height: 56 }}>
|
||||
<div style={{ width: 1, flex: 1, backgroundColor: "#d1d5db" }} />
|
||||
<button
|
||||
onClick={onInsert}
|
||||
className="flex items-center justify-center"
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: "50%",
|
||||
border: "1px solid #d1d5db",
|
||||
backgroundColor: "#fff",
|
||||
cursor: "pointer",
|
||||
zIndex: 1,
|
||||
transition: "all 0.15s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = "#6366f1";
|
||||
e.currentTarget.style.backgroundColor = "#eef2ff";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = "#d1d5db";
|
||||
e.currentTarget.style.backgroundColor = "#fff";
|
||||
}}
|
||||
title="Insert step"
|
||||
>
|
||||
<PlusIcon style={{ width: 12, height: 12, color: "#9ca3af" }} />
|
||||
</button>
|
||||
<div style={{ width: 1, flex: 1, backgroundColor: "#d1d5db" }} />
|
||||
</div>
|
||||
);
|
||||
const Connector: React.FC<ConnectorProps> = ({ onInsert }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col items-center" style={{ height: 56 }}>
|
||||
<div style={{ width: 1, flex: 1, backgroundColor: "#d1d5db" }} />
|
||||
<button
|
||||
onClick={onInsert}
|
||||
className="flex items-center justify-center"
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: "50%",
|
||||
border: "1px solid #d1d5db",
|
||||
backgroundColor: "#fff",
|
||||
cursor: "pointer",
|
||||
zIndex: 1,
|
||||
transition: "all 0.15s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = "#6366f1";
|
||||
e.currentTarget.style.backgroundColor = "#eef2ff";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = "#d1d5db";
|
||||
e.currentTarget.style.backgroundColor = "#fff";
|
||||
}}
|
||||
title={t("policies.pipelineFlowBuilder.insertStep")}
|
||||
>
|
||||
<PlusIcon style={{ width: 12, height: 12, color: "#9ca3af" }} />
|
||||
</button>
|
||||
<div style={{ width: 1, flex: 1, backgroundColor: "#d1d5db" }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Step Card (editable)
|
||||
|
|
@ -263,6 +268,8 @@ const StepCard: React.FC<StepCardProps> = ({
|
|||
onDelete,
|
||||
availableGuardrails,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const actionOptions = useMemo(() => getActionOptions(t), [t]);
|
||||
const guardrailOptions = availableGuardrails.map((g) => ({
|
||||
label: g.guardrail_name || g.guardrail_id,
|
||||
value: g.guardrail_name || g.guardrail_id,
|
||||
|
|
@ -292,11 +299,13 @@ const StepCard: React.FC<StepCardProps> = ({
|
|||
letterSpacing: "0.06em",
|
||||
}}
|
||||
>
|
||||
GUARDRAIL
|
||||
{t("policies.pipelineFlowBuilder.guardrailLabel")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span style={{ fontSize: 13, color: "#9ca3af" }}>Step {stepIndex + 1}</span>
|
||||
<span style={{ fontSize: 13, color: "#9ca3af" }}>
|
||||
{t("policies.pipelineFlowBuilder.stepN", { n: stepIndex + 1 })}
|
||||
</span>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
disabled={totalSteps <= 1}
|
||||
|
|
@ -309,7 +318,7 @@ const StepCard: React.FC<StepCardProps> = ({
|
|||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
title="Delete step"
|
||||
title={t("policies.pipelineFlowBuilder.deleteStep")}
|
||||
>
|
||||
<DotsVerticalIcon style={{ width: 16, height: 16, color: "#9ca3af" }} />
|
||||
</button>
|
||||
|
|
@ -319,12 +328,12 @@ const StepCard: React.FC<StepCardProps> = ({
|
|||
{/* Guardrail selector */}
|
||||
<div style={{ padding: "12px 20px 16px 20px" }}>
|
||||
<label style={{ fontSize: 12, fontWeight: 500, color: "#6b7280", display: "block", marginBottom: 6 }}>
|
||||
Guardrail
|
||||
{t("policies.pipelineFlowBuilder.guardrailLabel")}
|
||||
</label>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Select a guardrail"
|
||||
placeholder={t("policies.pipelineFlowBuilder.selectGuardrailPlaceholder")}
|
||||
value={step.guardrail || undefined}
|
||||
onChange={(value) => onChange({ guardrail: value })}
|
||||
options={guardrailOptions}
|
||||
|
|
@ -336,24 +345,26 @@ const StepCard: React.FC<StepCardProps> = ({
|
|||
<div style={{ borderTop: "1px solid #f0f0f0", padding: "14px 20px" }}>
|
||||
<div className="flex items-center gap-2" style={{ marginBottom: 8 }}>
|
||||
<PassIcon />
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: "#374151" }}>ON PASS</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: "#374151" }}>
|
||||
{t("policies.pipelineFlowBuilder.onPass")}
|
||||
</span>
|
||||
</div>
|
||||
<label style={{ fontSize: 12, fontWeight: 500, color: "#6b7280", display: "block", marginBottom: 6 }}>
|
||||
Action
|
||||
{t("policies.pipelineFlowBuilder.actionLabel")}
|
||||
</label>
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
value={step.on_pass}
|
||||
onChange={(value) => onChange({ on_pass: value as PipelineStep["on_pass"] })}
|
||||
options={ACTION_OPTIONS}
|
||||
options={actionOptions}
|
||||
/>
|
||||
{step.on_pass === "modify_response" && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<label style={{ fontSize: 12, fontWeight: 500, color: "#6b7280", display: "block", marginBottom: 6 }}>
|
||||
Custom Response Message
|
||||
{t("policies.pipelineFlowBuilder.customResponseMessage")}
|
||||
</label>
|
||||
<TextInput
|
||||
placeholder="Enter custom response..."
|
||||
placeholder={t("policies.pipelineFlowBuilder.enterCustomResponsePlaceholder")}
|
||||
value={step.modify_response_message || ""}
|
||||
onChange={(e) => onChange({ modify_response_message: e.target.value || null })}
|
||||
/>
|
||||
|
|
@ -365,24 +376,26 @@ const StepCard: React.FC<StepCardProps> = ({
|
|||
<div style={{ borderTop: "1px solid #f0f0f0", padding: "14px 20px" }}>
|
||||
<div className="flex items-center gap-2" style={{ marginBottom: 8 }}>
|
||||
<FailIcon />
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: "#374151" }}>ON FAIL</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: "#374151" }}>
|
||||
{t("policies.pipelineFlowBuilder.onFail")}
|
||||
</span>
|
||||
</div>
|
||||
<label style={{ fontSize: 12, fontWeight: 500, color: "#6b7280", display: "block", marginBottom: 6 }}>
|
||||
Action
|
||||
{t("policies.pipelineFlowBuilder.actionLabel")}
|
||||
</label>
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
value={step.on_fail}
|
||||
onChange={(value) => onChange({ on_fail: value as PipelineStep["on_fail"] })}
|
||||
options={ACTION_OPTIONS}
|
||||
options={actionOptions}
|
||||
/>
|
||||
{step.on_fail === "modify_response" && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<label style={{ fontSize: 12, fontWeight: 500, color: "#6b7280", display: "block", marginBottom: 6 }}>
|
||||
Custom Response Message
|
||||
{t("policies.pipelineFlowBuilder.customResponseMessage")}
|
||||
</label>
|
||||
<TextInput
|
||||
placeholder="Enter custom response..."
|
||||
placeholder={t("policies.pipelineFlowBuilder.enterCustomResponsePlaceholder")}
|
||||
value={step.modify_response_message || ""}
|
||||
onChange={(e) => onChange({ modify_response_message: e.target.value || null })}
|
||||
/>
|
||||
|
|
@ -394,14 +407,16 @@ const StepCard: React.FC<StepCardProps> = ({
|
|||
<div style={{ borderTop: "1px solid #f0f0f0", padding: "14px 20px" }}>
|
||||
<div className="flex items-center gap-2" style={{ marginBottom: 8 }}>
|
||||
<ApiFailureIcon />
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: "#374151" }}>ON API FAILURE</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: "#374151" }}>
|
||||
{t("policies.pipelineFlowBuilder.onApiFailure")}
|
||||
</span>
|
||||
</div>
|
||||
<label style={{ fontSize: 12, fontWeight: 500, color: "#6b7280", display: "block", marginBottom: 6 }}>
|
||||
Action
|
||||
{t("policies.pipelineFlowBuilder.actionLabel")}
|
||||
</label>
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Same as ON FAIL"
|
||||
placeholder={t("policies.pipelineFlowBuilder.sameAsOnFailPlaceholder")}
|
||||
allowClear
|
||||
value={step.on_error ?? undefined}
|
||||
onChange={(value) =>
|
||||
|
|
@ -409,15 +424,15 @@ const StepCard: React.FC<StepCardProps> = ({
|
|||
on_error: value === undefined || value === null ? undefined : (value as PipelineStep["on_error"]),
|
||||
})
|
||||
}
|
||||
options={ACTION_OPTIONS}
|
||||
options={actionOptions}
|
||||
/>
|
||||
{step.on_error === "modify_response" && step.on_fail !== "modify_response" && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<label style={{ fontSize: 12, fontWeight: 500, color: "#6b7280", display: "block", marginBottom: 6 }}>
|
||||
Custom Response Message
|
||||
{t("policies.pipelineFlowBuilder.customResponseMessage")}
|
||||
</label>
|
||||
<TextInput
|
||||
placeholder="Enter custom response..."
|
||||
placeholder={t("policies.pipelineFlowBuilder.enterCustomResponsePlaceholder")}
|
||||
value={step.modify_response_message || ""}
|
||||
onChange={(e) => onChange({ modify_response_message: e.target.value || null })}
|
||||
/>
|
||||
|
|
@ -439,6 +454,7 @@ interface PipelineFlowBuilderProps {
|
|||
}
|
||||
|
||||
const PipelineFlowBuilder: React.FC<PipelineFlowBuilderProps> = ({ pipeline, onChange, availableGuardrails }) => {
|
||||
const { t } = useTranslation();
|
||||
const handleInsertStep = (atIndex: number) => {
|
||||
onChange({ ...pipeline, steps: insertStep(pipeline.steps, atIndex) });
|
||||
};
|
||||
|
|
@ -481,12 +497,12 @@ const PipelineFlowBuilder: React.FC<PipelineFlowBuilderProps> = ({ pipeline, onC
|
|||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
TRIGGER
|
||||
{t("policies.pipelineFlowBuilder.triggerLabel")}
|
||||
</span>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: "#111827", display: "block" }}>
|
||||
Incoming LLM Request
|
||||
{t("policies.pipelineFlowBuilder.incomingLlmRequest")}
|
||||
</span>
|
||||
<span style={{ fontSize: 13, color: "#9ca3af" }}>This flow runs when a request matches this policy</span>
|
||||
<span style={{ fontSize: 13, color: "#9ca3af" }}>{t("policies.pipelineFlowBuilder.triggerDesc")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -559,10 +575,14 @@ const PipelineFlowBuilder: React.FC<PipelineFlowBuilderProps> = ({ pipeline, onC
|
|||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
END
|
||||
{t("policies.pipelineFlowBuilder.endLabel")}
|
||||
</span>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: "#111827", display: "block" }}>
|
||||
{t("policies.pipelineFlowBuilder.continueToLlm")}
|
||||
</span>
|
||||
<span style={{ fontSize: 13, color: "#9ca3af" }}>
|
||||
{t("policies.pipelineFlowBuilder.requestProceedsToModel")}
|
||||
</span>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: "#111827", display: "block" }}>Continue to LLM</span>
|
||||
<span style={{ fontSize: 13, color: "#9ca3af" }}>Request proceeds to the model</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -578,102 +598,116 @@ interface PipelineInfoDisplayProps {
|
|||
pipeline: GuardrailPipeline;
|
||||
}
|
||||
|
||||
export const PipelineInfoDisplay: React.FC<PipelineInfoDisplayProps> = ({ pipeline }) => (
|
||||
<div className="flex flex-col items-center" style={{ padding: "16px 0" }}>
|
||||
{/* Trigger */}
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #e5e7eb",
|
||||
borderRadius: 10,
|
||||
padding: "14px 20px",
|
||||
backgroundColor: "#fff",
|
||||
maxWidth: 720,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<PlayIcon />
|
||||
<div>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
textTransform: "uppercase",
|
||||
color: "#6b7280",
|
||||
letterSpacing: "0.06em",
|
||||
display: "block",
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
TRIGGER
|
||||
</span>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: "#111827" }}>Incoming LLM Request</span>
|
||||
export const PipelineInfoDisplay: React.FC<PipelineInfoDisplayProps> = ({ pipeline }) => {
|
||||
const { t } = useTranslation();
|
||||
const actionLabels = useMemo(() => getActionLabels(t), [t]);
|
||||
return (
|
||||
<div className="flex flex-col items-center" style={{ padding: "16px 0" }}>
|
||||
{/* Trigger */}
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #e5e7eb",
|
||||
borderRadius: 10,
|
||||
padding: "14px 20px",
|
||||
backgroundColor: "#fff",
|
||||
maxWidth: 720,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<PlayIcon />
|
||||
<div>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
textTransform: "uppercase",
|
||||
color: "#6b7280",
|
||||
letterSpacing: "0.06em",
|
||||
display: "block",
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
{t("policies.pipelineFlowBuilder.triggerLabel")}
|
||||
</span>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: "#111827" }}>
|
||||
{t("policies.pipelineFlowBuilder.incomingLlmRequest")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
{pipeline.steps.map((step, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{/* Connector */}
|
||||
<div style={{ width: 1, height: 32, backgroundColor: "#d1d5db" }} />
|
||||
{/* Steps */}
|
||||
{pipeline.steps.map((step, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{/* Connector */}
|
||||
<div style={{ width: 1, height: 32, backgroundColor: "#d1d5db" }} />
|
||||
|
||||
{/* Step card */}
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #e5e7eb",
|
||||
borderRadius: 10,
|
||||
padding: "14px 20px",
|
||||
backgroundColor: "#fff",
|
||||
maxWidth: 720,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between" style={{ marginBottom: 8 }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<GuardrailIcon />
|
||||
<span
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
textTransform: "uppercase",
|
||||
color: "#6366f1",
|
||||
letterSpacing: "0.06em",
|
||||
}}
|
||||
>
|
||||
GUARDRAIL
|
||||
{/* Step card */}
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #e5e7eb",
|
||||
borderRadius: 10,
|
||||
padding: "14px 20px",
|
||||
backgroundColor: "#fff",
|
||||
maxWidth: 720,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between" style={{ marginBottom: 8 }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<GuardrailIcon />
|
||||
<span
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
textTransform: "uppercase",
|
||||
color: "#6366f1",
|
||||
letterSpacing: "0.06em",
|
||||
}}
|
||||
>
|
||||
{t("policies.pipelineFlowBuilder.guardrailLabel")}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ fontSize: 13, color: "#9ca3af" }}>
|
||||
{t("policies.pipelineFlowBuilder.stepN", { n: index + 1 })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div style={{ fontSize: 15, fontWeight: 600, color: "#111827", marginBottom: 8 }}>{step.guardrail}</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div style={{ borderTop: "1px solid #f3f4f6", marginBottom: 10 }} />
|
||||
|
||||
{/* Pass / Fail / API failure */}
|
||||
<div className="flex flex-col gap-2" style={{ fontSize: 13, color: "#374151" }}>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<PassIcon />{" "}
|
||||
{t("policies.pipelineFlowBuilder.passArrow", { action: actionLabels[step.on_pass] || step.on_pass })}
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<FailIcon />{" "}
|
||||
{t("policies.pipelineFlowBuilder.onFailArrow", { action: actionLabels[step.on_fail] || step.on_fail })}
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<ApiFailureIcon />{" "}
|
||||
{step.on_error != null
|
||||
? t("policies.pipelineFlowBuilder.onApiFailureArrow", {
|
||||
action: actionLabels[step.on_error] || step.on_error,
|
||||
})
|
||||
: t("policies.pipelineFlowBuilder.onApiFailureArrowSameAsFail", {
|
||||
action: actionLabels[step.on_fail] || step.on_fail,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ fontSize: 13, color: "#9ca3af" }}>Step {index + 1}</span>
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div style={{ fontSize: 15, fontWeight: 600, color: "#111827", marginBottom: 8 }}>{step.guardrail}</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div style={{ borderTop: "1px solid #f3f4f6", marginBottom: 10 }} />
|
||||
|
||||
{/* Pass / Fail / API failure */}
|
||||
<div className="flex flex-col gap-2" style={{ fontSize: 13, color: "#374151" }}>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<PassIcon /> Pass → {ACTION_LABELS[step.on_pass] || step.on_pass}
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<FailIcon /> On fail → {ACTION_LABELS[step.on_fail] || step.on_fail}
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<ApiFailureIcon /> On API failure →{" "}
|
||||
{step.on_error != null
|
||||
? ACTION_LABELS[step.on_error] || step.on_error
|
||||
: `${ACTION_LABELS[step.on_fail] || step.on_fail} (same as on fail)`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Pipeline Test Panel (right drawer)
|
||||
|
|
@ -685,11 +719,11 @@ interface PipelineTestPanelProps {
|
|||
onClose: () => void;
|
||||
}
|
||||
|
||||
const OUTCOME_STYLES: Record<string, { bg: string; color: string; label: string }> = {
|
||||
pass: { bg: "#f0fdf4", color: "#16a34a", label: "PASS" },
|
||||
fail: { bg: "#fef2f2", color: "#dc2626", label: "FAIL" },
|
||||
error: { bg: "#fffbeb", color: "#d97706", label: "ERROR" },
|
||||
};
|
||||
const getOutcomeStyles = (t: TFunction): Record<string, { bg: string; color: string; label: string }> => ({
|
||||
pass: { bg: "#f0fdf4", color: "#16a34a", label: t("policies.pipelineFlowBuilder.outcomePass") },
|
||||
fail: { bg: "#fef2f2", color: "#dc2626", label: t("policies.pipelineFlowBuilder.outcomeFail") },
|
||||
error: { bg: "#fffbeb", color: "#d97706", label: t("policies.pipelineFlowBuilder.outcomeError") },
|
||||
});
|
||||
|
||||
const TERMINAL_STYLES: Record<string, { bg: string; color: string }> = {
|
||||
allow: { bg: "#f0fdf4", color: "#16a34a" },
|
||||
|
|
@ -711,13 +745,17 @@ function complianceMatchExpected(expected: "pass" | "fail", terminalAction: stri
|
|||
return terminalAction === "block";
|
||||
}
|
||||
|
||||
const testSourceOptions = [
|
||||
{ value: TEST_SOURCE_QUICK, label: "Quick chat (custom message)" },
|
||||
const getTestSourceOptions = (t: TFunction) => [
|
||||
{ value: TEST_SOURCE_QUICK, label: t("policies.pipelineFlowBuilder.testSourceQuickChat") },
|
||||
...getFrameworks().map((f) => ({ value: f.name, label: f.name })),
|
||||
{ value: TEST_SOURCE_ALL, label: "All compliance datasets" },
|
||||
{ value: TEST_SOURCE_ALL, label: t("policies.pipelineFlowBuilder.testSourceAllDatasets") },
|
||||
];
|
||||
|
||||
const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessToken, onClose }) => {
|
||||
const { t } = useTranslation();
|
||||
const testSourceOptions = useMemo(() => getTestSourceOptions(t), [t]);
|
||||
const outcomeStyles = useMemo(() => getOutcomeStyles(t), [t]);
|
||||
const actionLabels = useMemo(() => getActionLabels(t), [t]);
|
||||
const [testSource, setTestSource] = useState<string>(TEST_SOURCE_QUICK);
|
||||
const [testMessage, setTestMessage] = useState("Hello, can you help me?");
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
|
|
@ -734,7 +772,7 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
|
|||
|
||||
const emptySteps = pipeline.steps.filter((s) => !s.guardrail);
|
||||
if (emptySteps.length > 0) {
|
||||
setError("All steps must have a guardrail selected");
|
||||
setError(t("policies.pipelineFlowBuilder.allStepsMustHaveGuardrail"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -797,7 +835,9 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
|
|||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: "#111827" }}>Test Pipeline</span>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: "#111827" }}>
|
||||
{t("policies.pipelineFlowBuilder.testPipelineTitle")}
|
||||
</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
|
|
@ -816,7 +856,7 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
|
|||
{/* Input section */}
|
||||
<div style={{ padding: 16, borderBottom: "1px solid #e5e7eb" }}>
|
||||
<label style={{ fontSize: 12, fontWeight: 500, color: "#6b7280", display: "block", marginBottom: 6 }}>
|
||||
Test with
|
||||
{t("policies.pipelineFlowBuilder.testWithLabel")}
|
||||
</label>
|
||||
<Select
|
||||
value={testSource}
|
||||
|
|
@ -828,12 +868,12 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
|
|||
{isQuickChat && (
|
||||
<>
|
||||
<label style={{ fontSize: 12, fontWeight: 500, color: "#6b7280", display: "block", marginBottom: 6 }}>
|
||||
Message
|
||||
{t("policies.pipelineFlowBuilder.messageLabel")}
|
||||
</label>
|
||||
<textarea
|
||||
value={testMessage}
|
||||
onChange={(e) => setTestMessage(e.target.value)}
|
||||
placeholder="Enter a test message..."
|
||||
placeholder={t("policies.pipelineFlowBuilder.enterTestMessagePlaceholder")}
|
||||
rows={3}
|
||||
style={{
|
||||
width: "100%",
|
||||
|
|
@ -859,12 +899,15 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
|
|||
}}
|
||||
>
|
||||
{testSource === TEST_SOURCE_ALL
|
||||
? "Run pipeline against all compliance prompts (EU AI Act, GDPR, Topic Blocking, Airline, etc.)."
|
||||
: `Run pipeline against ${promptsForSource.length} prompts from "${testSource}".`}
|
||||
? t("policies.pipelineFlowBuilder.runAgainstAllCompliance")
|
||||
: t("policies.pipelineFlowBuilder.runAgainstDataset", {
|
||||
count: promptsForSource.length,
|
||||
source: testSource,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<Button onClick={handleRunTest} loading={isRunning} style={{ marginTop: 8, width: "100%" }}>
|
||||
Run Test
|
||||
{t("policies.pipelineFlowBuilder.runTest")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -890,7 +933,7 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
|
|||
<div>
|
||||
{/* Step results */}
|
||||
{result.step_results.map((step, i) => {
|
||||
const style = OUTCOME_STYLES[step.outcome] || OUTCOME_STYLES.error;
|
||||
const style = outcomeStyles[step.outcome] || outcomeStyles.error;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
|
|
@ -903,7 +946,7 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
|
|||
>
|
||||
<div className="flex items-center justify-between" style={{ marginBottom: 4 }}>
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: "#111827" }}>
|
||||
Step {i + 1}: {step.guardrail_name}
|
||||
{t("policies.pipelineFlowBuilder.stepNGuardrail", { n: i + 1, name: step.guardrail_name })}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
|
|
@ -919,7 +962,9 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
|
|||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#6b7280" }}>
|
||||
Action: {ACTION_LABELS[step.action_taken] || step.action_taken}
|
||||
{t("policies.pipelineFlowBuilder.actionTaken", {
|
||||
action: actionLabels[step.action_taken] || step.action_taken,
|
||||
})}
|
||||
{step.duration_seconds != null && (
|
||||
<span style={{ marginLeft: 8 }}>({(step.duration_seconds * 1000).toFixed(0)}ms)</span>
|
||||
)}
|
||||
|
|
@ -940,7 +985,9 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
|
|||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: "#111827" }}>Result</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: "#111827" }}>
|
||||
{t("policies.pipelineFlowBuilder.resultLabel")}
|
||||
</span>
|
||||
{(() => {
|
||||
const ts = TERMINAL_STYLES[result.terminal_action] || TERMINAL_STYLES.block;
|
||||
return (
|
||||
|
|
@ -955,7 +1002,9 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
|
|||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
{result.terminal_action === "modify_response" ? "Custom Response" : result.terminal_action}
|
||||
{result.terminal_action === "modify_response"
|
||||
? t("policies.pipelineFlowBuilder.actionCustomResponse")
|
||||
: result.terminal_action}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
|
|
@ -965,7 +1014,7 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
|
|||
)}
|
||||
{result.modify_response_message && (
|
||||
<div style={{ fontSize: 12, color: "#2563eb", marginTop: 6 }}>
|
||||
Response: {result.modify_response_message}
|
||||
{t("policies.pipelineFlowBuilder.responseLabel", { msg: result.modify_response_message })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -982,7 +1031,7 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
|
|||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
Compliance dataset
|
||||
{t("policies.pipelineFlowBuilder.complianceDataset")}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -991,7 +1040,10 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
|
|||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
{complianceResults.filter((e) => e.matched).length} / {complianceResults.length} matched expected
|
||||
{t("policies.pipelineFlowBuilder.complianceMatchedExpected", {
|
||||
matched: complianceResults.filter((e) => e.matched).length,
|
||||
total: complianceResults.length,
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -1035,9 +1087,13 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
|
|||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: "#6b7280" }}>expected: {entry.prompt.expectedResult}</span>
|
||||
<span style={{ color: "#6b7280" }}>
|
||||
{t("policies.pipelineFlowBuilder.expectedLabel", { value: entry.prompt.expectedResult })}
|
||||
</span>
|
||||
<span style={{ color: "#9ca3af" }}>→</span>
|
||||
<span style={{ color: "#6b7280" }}>actual: {actual}</span>
|
||||
<span style={{ color: "#6b7280" }}>
|
||||
{t("policies.pipelineFlowBuilder.actualLabel", { value: actual })}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: matchStyle.bg,
|
||||
|
|
@ -1060,7 +1116,7 @@ const PipelineTestPanel: React.FC<PipelineTestPanelProps> = ({ pipeline, accessT
|
|||
|
||||
{!result && !error && complianceResults.length === 0 && (
|
||||
<div style={{ textAlign: "center", color: "#9ca3af", fontSize: 13, marginTop: 24 }}>
|
||||
Choose a test source above (quick chat or a compliance dataset) and click "Run Test"
|
||||
{t("policies.pipelineFlowBuilder.chooseTestSourceHint")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -1107,6 +1163,7 @@ const PolicyVersionsSidebar: React.FC<PolicyVersionsSidebarProps> = ({
|
|||
onPublish,
|
||||
onPromoteToProduction,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const canPublish = editingVersionStatus === "draft" && onPublish;
|
||||
const canPromote = editingVersionStatus === "published" && onPromoteToProduction;
|
||||
|
||||
|
|
@ -1136,7 +1193,7 @@ const PolicyVersionsSidebar: React.FC<PolicyVersionsSidebarProps> = ({
|
|||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
Versions
|
||||
{t("policies.pipelineFlowBuilder.versionsLabel")}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
|
|
@ -1147,7 +1204,7 @@ const PolicyVersionsSidebar: React.FC<PolicyVersionsSidebarProps> = ({
|
|||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
Production = the version used when anyone calls this policy by name.
|
||||
{t("policies.pipelineFlowBuilder.productionVersionDesc")}
|
||||
</span>
|
||||
<Button
|
||||
onClick={onNewVersion}
|
||||
|
|
@ -1155,14 +1212,14 @@ const PolicyVersionsSidebar: React.FC<PolicyVersionsSidebarProps> = ({
|
|||
loading={isCreatingVersion}
|
||||
style={{ width: "100%", marginBottom: 12 }}
|
||||
>
|
||||
+ New Version
|
||||
{t("policies.pipelineFlowBuilder.newVersion")}
|
||||
</Button>
|
||||
{isLoading ? (
|
||||
<div style={{ display: "flex", justifyContent: "center", padding: 16 }}>
|
||||
<Spin size="small" />
|
||||
</div>
|
||||
) : versions.length === 0 ? (
|
||||
<span style={{ fontSize: 13, color: "#9ca3af" }}>No versions found</span>
|
||||
<span style={{ fontSize: 13, color: "#9ca3af" }}>{t("policies.pipelineFlowBuilder.noVersionsFound")}</span>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
{versions.map((v) => {
|
||||
|
|
@ -1217,7 +1274,7 @@ const PolicyVersionsSidebar: React.FC<PolicyVersionsSidebarProps> = ({
|
|||
loading={isUpdatingStatus}
|
||||
style={{ width: "100%", marginBottom: 8 }}
|
||||
>
|
||||
Publish
|
||||
{t("policies.pipelineFlowBuilder.publish")}
|
||||
</Button>
|
||||
<span
|
||||
style={{
|
||||
|
|
@ -1228,7 +1285,7 @@ const PolicyVersionsSidebar: React.FC<PolicyVersionsSidebarProps> = ({
|
|||
marginBottom: canPromote ? 8 : 0,
|
||||
}}
|
||||
>
|
||||
Published versions can be tested in the Playground before promoting to production.
|
||||
{t("policies.pipelineFlowBuilder.publishedVersionsDesc")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -1240,7 +1297,7 @@ const PolicyVersionsSidebar: React.FC<PolicyVersionsSidebarProps> = ({
|
|||
loading={isUpdatingStatus}
|
||||
style={{ width: "100%", marginBottom: 8 }}
|
||||
>
|
||||
Promote to production
|
||||
{t("policies.pipelineFlowBuilder.promoteToProduction")}
|
||||
</Button>
|
||||
<span
|
||||
style={{
|
||||
|
|
@ -1250,7 +1307,7 @@ const PolicyVersionsSidebar: React.FC<PolicyVersionsSidebarProps> = ({
|
|||
display: "block",
|
||||
}}
|
||||
>
|
||||
This version will be used when anyone calls this policy by name.
|
||||
{t("policies.pipelineFlowBuilder.promoteToProductionDesc")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -1270,7 +1327,7 @@ const PolicyVersionsSidebar: React.FC<PolicyVersionsSidebarProps> = ({
|
|||
letterSpacing: "0.06em",
|
||||
}}
|
||||
>
|
||||
Silent Mirroring
|
||||
{t("policies.pipelineFlowBuilder.silentMirroring")}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
|
|
@ -1282,7 +1339,7 @@ const PolicyVersionsSidebar: React.FC<PolicyVersionsSidebarProps> = ({
|
|||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
COMING SOON
|
||||
{t("common.comingSoon")}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
|
|
@ -1293,8 +1350,7 @@ const PolicyVersionsSidebar: React.FC<PolicyVersionsSidebarProps> = ({
|
|||
display: "block",
|
||||
}}
|
||||
>
|
||||
Test policy versions on production traffic without blocking requests. Shadow testing helps validate changes
|
||||
before full rollout.
|
||||
{t("policies.pipelineFlowBuilder.silentMirroringDesc")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1331,6 +1387,7 @@ export const FlowBuilderPage: React.FC<FlowBuilderPageProps> = ({
|
|||
onSelectVersion,
|
||||
onVersionStatusUpdated,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const isEditing = !!editingPolicy?.policy_id;
|
||||
const showVersionsSidebar = !!editingPolicy?.policy_name;
|
||||
|
||||
|
|
@ -1385,13 +1442,15 @@ export const FlowBuilderPage: React.FC<FlowBuilderPageProps> = ({
|
|||
setIsCreatingVersion(true);
|
||||
try {
|
||||
const newPolicy = await createPolicyVersion(accessToken, editingPolicy.policy_name);
|
||||
NotificationsManager.success("New draft version created");
|
||||
NotificationsManager.success(t("policies.pipelineFlowBuilder.newDraftVersionCreated"));
|
||||
onVersionCreated?.(newPolicy);
|
||||
const list = await listPolicyVersions(accessToken, editingPolicy.policy_name);
|
||||
setVersions(list.versions ?? []);
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend(
|
||||
"Failed to create version: " + (error instanceof Error ? error.message : String(error)),
|
||||
t("policies.pipelineFlowBuilder.failedToCreateVersion", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setIsCreatingVersion(false);
|
||||
|
|
@ -1407,15 +1466,15 @@ export const FlowBuilderPage: React.FC<FlowBuilderPageProps> = ({
|
|||
setIsUpdatingStatus(true);
|
||||
try {
|
||||
const updated = await updatePolicyVersionStatus(accessToken, editingPolicy.policy_id, "published");
|
||||
NotificationsManager.success(
|
||||
"Version published. You can test it in the Playground by selecting this version in the Policies dropdown.",
|
||||
);
|
||||
NotificationsManager.success(t("policies.pipelineFlowBuilder.versionPublished"));
|
||||
const list = await listPolicyVersions(accessToken, editingPolicy.policy_name ?? "");
|
||||
setVersions(list.versions ?? []);
|
||||
onVersionStatusUpdated?.(updated);
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend(
|
||||
"Failed to publish: " + (error instanceof Error ? error.message : String(error)),
|
||||
t("policies.pipelineFlowBuilder.failedToPublish", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setIsUpdatingStatus(false);
|
||||
|
|
@ -1427,13 +1486,15 @@ export const FlowBuilderPage: React.FC<FlowBuilderPageProps> = ({
|
|||
setIsUpdatingStatus(true);
|
||||
try {
|
||||
const updated = await updatePolicyVersionStatus(accessToken, editingPolicy.policy_id, "production");
|
||||
NotificationsManager.success("Version promoted to production");
|
||||
NotificationsManager.success(t("policies.pipelineFlowBuilder.versionPromotedToProduction"));
|
||||
const list = await listPolicyVersions(accessToken, editingPolicy.policy_name ?? "");
|
||||
setVersions(list.versions ?? []);
|
||||
onVersionStatusUpdated?.(updated);
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend(
|
||||
"Failed to promote to production: " + (error instanceof Error ? error.message : String(error)),
|
||||
t("policies.pipelineFlowBuilder.failedToPromote", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setIsUpdatingStatus(false);
|
||||
|
|
@ -1442,17 +1503,17 @@ export const FlowBuilderPage: React.FC<FlowBuilderPageProps> = ({
|
|||
|
||||
const handleSave = async () => {
|
||||
if (!policyName.trim()) {
|
||||
MessageManager.error("Please enter a policy name");
|
||||
MessageManager.error(t("policies.pipelineFlowBuilder.enterPolicyNameError"));
|
||||
return;
|
||||
}
|
||||
if (!accessToken) {
|
||||
MessageManager.error("No access token available");
|
||||
MessageManager.error(t("policies.pipelineFlowBuilder.noAccessTokenError"));
|
||||
return;
|
||||
}
|
||||
|
||||
const emptySteps = pipeline.steps.filter((s) => !s.guardrail);
|
||||
if (emptySteps.length > 0) {
|
||||
MessageManager.error("Please select a guardrail for all steps");
|
||||
MessageManager.error(t("policies.pipelineFlowBuilder.selectGuardrailForAllStepsError"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1470,18 +1531,20 @@ export const FlowBuilderPage: React.FC<FlowBuilderPageProps> = ({
|
|||
|
||||
if (isEditing && editingPolicy) {
|
||||
await updatePolicy(accessToken, editingPolicy.policy_id, data as PolicyUpdateRequest);
|
||||
NotificationsManager.success("Policy updated successfully");
|
||||
NotificationsManager.success(t("policies.pipelineFlowBuilder.policyUpdatedSuccess"));
|
||||
onSuccess();
|
||||
} else {
|
||||
await createPolicy(accessToken, data as PolicyCreateRequest);
|
||||
NotificationsManager.success("Policy created successfully");
|
||||
NotificationsManager.success(t("policies.pipelineFlowBuilder.policyCreatedSuccess"));
|
||||
onSuccess();
|
||||
onBack();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save policy:", error);
|
||||
NotificationsManager.fromBackend(
|
||||
"Failed to save policy: " + (error instanceof Error ? error.message : String(error)),
|
||||
t("policies.pipelineFlowBuilder.failedToSavePolicy", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
|
@ -1529,10 +1592,10 @@ export const FlowBuilderPage: React.FC<FlowBuilderPageProps> = ({
|
|||
>
|
||||
<ArrowLeftIcon style={{ width: 18, height: 18, color: "#6b7280" }} />
|
||||
</button>
|
||||
<span style={{ fontSize: 14, color: "#6b7280" }}>Policies</span>
|
||||
<span style={{ fontSize: 14, color: "#6b7280" }}>{t("policies.pipelineFlowBuilder.policiesBreadcrumb")}</span>
|
||||
<span style={{ fontSize: 14, color: "#d1d5db" }}>/</span>
|
||||
<TextInput
|
||||
placeholder="Policy name..."
|
||||
placeholder={t("policies.pipelineFlowBuilder.policyNamePlaceholder")}
|
||||
value={policyName}
|
||||
onChange={(e) => setPolicyName(e.target.value)}
|
||||
disabled={isEditing}
|
||||
|
|
@ -1549,18 +1612,20 @@ export const FlowBuilderPage: React.FC<FlowBuilderPageProps> = ({
|
|||
letterSpacing: "0.02em",
|
||||
}}
|
||||
>
|
||||
Flow
|
||||
{t("policies.pipelineFlowBuilder.flowBadge")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="secondary" onClick={onBack}>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => setShowTestPanel(!showTestPanel)}>
|
||||
{showTestPanel ? "Hide Test" : "Test Pipeline"}
|
||||
{showTestPanel
|
||||
? t("policies.pipelineFlowBuilder.hideTest")
|
||||
: t("policies.pipelineFlowBuilder.testPipeline")}
|
||||
</Button>
|
||||
<Button onClick={handleSave} loading={isSubmitting}>
|
||||
{isEditing ? "Update Policy" : "Save Policy"}
|
||||
{isEditing ? t("policies.pipelineFlowBuilder.updatePolicy") : t("policies.pipelineFlowBuilder.savePolicy")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1575,7 +1640,7 @@ export const FlowBuilderPage: React.FC<FlowBuilderPageProps> = ({
|
|||
}}
|
||||
>
|
||||
<TextInput
|
||||
placeholder="Add a description (optional)..."
|
||||
placeholder={t("policies.pipelineFlowBuilder.addDescriptionPlaceholder")}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
style={{ maxWidth: 500 }}
|
||||
|
|
|
|||
|
|
@ -4898,6 +4898,86 @@
|
|||
"templateReady": "Template ready! Complete the policy form to save.",
|
||||
"guardrailsCreateFailed": "Failed to create {{count}} guardrail(s): {{list}}. You may need to create them manually.",
|
||||
"createGuardrailsFailed": "Failed to create guardrails. Please try again."
|
||||
},
|
||||
"pipelineFlowBuilder": {
|
||||
"actionNextStep": "Next Step",
|
||||
"actionAllow": "Allow",
|
||||
"actionBlock": "Block",
|
||||
"actionCustomResponse": "Custom Response",
|
||||
"insertStep": "Insert step",
|
||||
"guardrailLabel": "GUARDRAIL",
|
||||
"stepN": "Step {{n}}",
|
||||
"deleteStep": "Delete step",
|
||||
"selectGuardrailPlaceholder": "Select a guardrail",
|
||||
"onPass": "ON PASS",
|
||||
"onFail": "ON FAIL",
|
||||
"onApiFailure": "ON API FAILURE",
|
||||
"actionLabel": "Action",
|
||||
"customResponseMessage": "Custom Response Message",
|
||||
"enterCustomResponsePlaceholder": "Enter custom response...",
|
||||
"sameAsOnFailPlaceholder": "Same as ON FAIL",
|
||||
"triggerLabel": "TRIGGER",
|
||||
"incomingLlmRequest": "Incoming LLM Request",
|
||||
"triggerDesc": "This flow runs when a request matches this policy",
|
||||
"endLabel": "END",
|
||||
"continueToLlm": "Continue to LLM",
|
||||
"requestProceedsToModel": "Request proceeds to the model",
|
||||
"passArrow": "Pass → {{action}}",
|
||||
"onFailArrow": "On fail → {{action}}",
|
||||
"onApiFailureArrow": "On API failure → {{action}}",
|
||||
"onApiFailureArrowSameAsFail": "On API failure → {{action}} (same as on fail)",
|
||||
"outcomePass": "PASS",
|
||||
"outcomeFail": "FAIL",
|
||||
"outcomeError": "ERROR",
|
||||
"testSourceQuickChat": "Quick chat (custom message)",
|
||||
"testSourceAllDatasets": "All compliance datasets",
|
||||
"allStepsMustHaveGuardrail": "All steps must have a guardrail selected",
|
||||
"testPipelineTitle": "Test Pipeline",
|
||||
"testWithLabel": "Test with",
|
||||
"messageLabel": "Message",
|
||||
"enterTestMessagePlaceholder": "Enter a test message...",
|
||||
"runAgainstAllCompliance": "Run pipeline against all compliance prompts (EU AI Act, GDPR, Topic Blocking, Airline, etc.).",
|
||||
"runAgainstDataset": "Run pipeline against {{count}} prompts from \"{{source}}\".",
|
||||
"runTest": "Run Test",
|
||||
"stepNGuardrail": "Step {{n}}: {{name}}",
|
||||
"actionTaken": "Action: {{action}}",
|
||||
"resultLabel": "Result",
|
||||
"responseLabel": "Response: {{msg}}",
|
||||
"complianceDataset": "Compliance dataset",
|
||||
"complianceMatchedExpected": "{{matched}} / {{total}} matched expected",
|
||||
"expectedLabel": "expected: {{value}}",
|
||||
"actualLabel": "actual: {{value}}",
|
||||
"chooseTestSourceHint": "Choose a test source above (quick chat or a compliance dataset) and click \"Run Test\"",
|
||||
"versionsLabel": "Versions",
|
||||
"productionVersionDesc": "Production = the version used when anyone calls this policy by name.",
|
||||
"newVersion": "+ New Version",
|
||||
"noVersionsFound": "No versions found",
|
||||
"publish": "Publish",
|
||||
"publishedVersionsDesc": "Published versions can be tested in the Playground before promoting to production.",
|
||||
"promoteToProduction": "Promote to production",
|
||||
"promoteToProductionDesc": "This version will be used when anyone calls this policy by name.",
|
||||
"silentMirroring": "Silent Mirroring",
|
||||
"silentMirroringDesc": "Test policy versions on production traffic without blocking requests. Shadow testing helps validate changes before full rollout.",
|
||||
"newDraftVersionCreated": "New draft version created",
|
||||
"failedToCreateVersion": "Failed to create version: {{error}}",
|
||||
"versionPublished": "Version published. You can test it in the Playground by selecting this version in the Policies dropdown.",
|
||||
"failedToPublish": "Failed to publish: {{error}}",
|
||||
"versionPromotedToProduction": "Version promoted to production",
|
||||
"failedToPromote": "Failed to promote to production: {{error}}",
|
||||
"enterPolicyNameError": "Please enter a policy name",
|
||||
"noAccessTokenError": "No access token available",
|
||||
"selectGuardrailForAllStepsError": "Please select a guardrail for all steps",
|
||||
"policyUpdatedSuccess": "Policy updated successfully",
|
||||
"policyCreatedSuccess": "Policy created successfully",
|
||||
"failedToSavePolicy": "Failed to save policy: {{error}}",
|
||||
"policiesBreadcrumb": "Policies",
|
||||
"policyNamePlaceholder": "Policy name...",
|
||||
"flowBadge": "Flow",
|
||||
"hideTest": "Hide Test",
|
||||
"testPipeline": "Test Pipeline",
|
||||
"updatePolicy": "Update Policy",
|
||||
"savePolicy": "Save Policy",
|
||||
"addDescriptionPlaceholder": "Add a description (optional)..."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4898,6 +4898,86 @@
|
|||
"templateReady": "模板已就绪!请完成策略表单以保存。",
|
||||
"guardrailsCreateFailed": "{{count}} 个护栏创建失败:{{list}}。您可能需要手动创建。",
|
||||
"createGuardrailsFailed": "创建护栏失败,请重试。"
|
||||
},
|
||||
"pipelineFlowBuilder": {
|
||||
"actionNextStep": "下一步",
|
||||
"actionAllow": "允许",
|
||||
"actionBlock": "拦截",
|
||||
"actionCustomResponse": "自定义响应",
|
||||
"insertStep": "插入步骤",
|
||||
"guardrailLabel": "护栏",
|
||||
"stepN": "步骤 {{n}}",
|
||||
"deleteStep": "删除步骤",
|
||||
"selectGuardrailPlaceholder": "选择护栏",
|
||||
"onPass": "通过时",
|
||||
"onFail": "失败时",
|
||||
"onApiFailure": "API 故障时",
|
||||
"actionLabel": "动作",
|
||||
"customResponseMessage": "自定义响应内容",
|
||||
"enterCustomResponsePlaceholder": "输入自定义响应...",
|
||||
"sameAsOnFailPlaceholder": "与失败时相同",
|
||||
"triggerLabel": "触发器",
|
||||
"incomingLlmRequest": "传入 LLM 请求",
|
||||
"triggerDesc": "当请求匹配此策略时,该流程将运行",
|
||||
"endLabel": "结束",
|
||||
"continueToLlm": "继续至 LLM",
|
||||
"requestProceedsToModel": "请求将继续发送到模型",
|
||||
"passArrow": "通过 → {{action}}",
|
||||
"onFailArrow": "失败时 → {{action}}",
|
||||
"onApiFailureArrow": "API 故障时 → {{action}}",
|
||||
"onApiFailureArrowSameAsFail": "API 故障时 → {{action}}(与失败时相同)",
|
||||
"outcomePass": "通过",
|
||||
"outcomeFail": "失败",
|
||||
"outcomeError": "错误",
|
||||
"testSourceQuickChat": "快速对话(自定义消息)",
|
||||
"testSourceAllDatasets": "所有合规数据集",
|
||||
"allStepsMustHaveGuardrail": "所有步骤都必须选择护栏",
|
||||
"testPipelineTitle": "测试流水线",
|
||||
"testWithLabel": "测试方式",
|
||||
"messageLabel": "消息",
|
||||
"enterTestMessagePlaceholder": "输入测试消息...",
|
||||
"runAgainstAllCompliance": "对所有合规提示词运行流水线(EU AI Act、GDPR、话题拦截、航空等)。",
|
||||
"runAgainstDataset": "对「{{source}}」中的 {{count}} 条提示词运行流水线。",
|
||||
"runTest": "运行测试",
|
||||
"stepNGuardrail": "步骤 {{n}}:{{name}}",
|
||||
"actionTaken": "动作:{{action}}",
|
||||
"resultLabel": "结果",
|
||||
"responseLabel": "响应:{{msg}}",
|
||||
"complianceDataset": "合规数据集",
|
||||
"complianceMatchedExpected": "{{matched}} / {{total}} 条符合预期",
|
||||
"expectedLabel": "预期:{{value}}",
|
||||
"actualLabel": "实际:{{value}}",
|
||||
"chooseTestSourceHint": "请在上方选择测试源(快速对话或合规数据集),然后点击「运行测试」",
|
||||
"versionsLabel": "版本",
|
||||
"productionVersionDesc": "生产版本 = 任何人通过名称调用此策略时所使用的版本。",
|
||||
"newVersion": "+ 新版本",
|
||||
"noVersionsFound": "未找到版本",
|
||||
"publish": "发布",
|
||||
"publishedVersionsDesc": "已发布的版本可在 Playground 中测试后再推广到生产环境。",
|
||||
"promoteToProduction": "推广到生产",
|
||||
"promoteToProductionDesc": "任何人通过名称调用此策略时将使用此版本。",
|
||||
"silentMirroring": "静默镜像",
|
||||
"silentMirroringDesc": "在生产流量上测试策略版本,且不拦截请求。影子测试有助于在全面发布前验证变更。",
|
||||
"newDraftVersionCreated": "新草稿版本已创建",
|
||||
"failedToCreateVersion": "创建版本失败:{{error}}",
|
||||
"versionPublished": "版本已发布。您可以在 Playground 中通过策略下拉菜单选择此版本进行测试。",
|
||||
"failedToPublish": "发布失败:{{error}}",
|
||||
"versionPromotedToProduction": "版本已推广到生产",
|
||||
"failedToPromote": "推广到生产失败:{{error}}",
|
||||
"enterPolicyNameError": "请输入策略名称",
|
||||
"noAccessTokenError": "无可用访问令牌",
|
||||
"selectGuardrailForAllStepsError": "请为所有步骤选择护栏",
|
||||
"policyUpdatedSuccess": "策略更新成功",
|
||||
"policyCreatedSuccess": "策略创建成功",
|
||||
"failedToSavePolicy": "保存策略失败:{{error}}",
|
||||
"policiesBreadcrumb": "策略",
|
||||
"policyNamePlaceholder": "策略名称...",
|
||||
"flowBadge": "流程",
|
||||
"hideTest": "隐藏测试",
|
||||
"testPipeline": "测试流水线",
|
||||
"updatePolicy": "更新策略",
|
||||
"savePolicy": "保存策略",
|
||||
"addDescriptionPlaceholder": "添加描述(可选)..."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue