Restyle flow builder to match dev-tool UI aesthetic

This commit is contained in:
Ishaan Jaffer 2026-02-13 19:18:32 -08:00
parent 4341aed641
commit aac69c846b

View file

@ -1,5 +1,5 @@
import React, { useState } from "react";
import { Select, Typography, Tag, message } from "antd";
import { Select, Typography, message } from "antd";
import { Button, TextInput } from "@tremor/react";
import { ArrowLeftIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline";
import { GuardrailPipeline, PipelineStep, PolicyCreateRequest, PolicyUpdateRequest, Policy } from "./types";
@ -55,39 +55,36 @@ interface ConnectorProps {
}
const Connector: React.FC<ConnectorProps> = ({ onInsert }) => (
<div className="flex flex-col items-center" style={{ height: 48 }}>
<div
style={{
width: 2,
flex: 1,
backgroundColor: "#d9d9d9",
}}
/>
<div className="flex flex-col items-center" style={{ height: 28 }}>
<div style={{ width: 1, flex: 1, backgroundColor: "#e5e7eb" }} />
<button
onClick={onInsert}
className="flex items-center justify-center"
style={{
width: 28,
height: 28,
width: 20,
height: 20,
borderRadius: "50%",
border: "2px solid #d9d9d9",
border: "1px solid #e5e7eb",
backgroundColor: "#fff",
cursor: "pointer",
zIndex: 1,
marginTop: -4,
marginBottom: -4,
marginTop: -2,
marginBottom: -2,
transition: "all 0.15s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = "#9ca3af";
e.currentTarget.style.backgroundColor = "#f9fafb";
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = "#e5e7eb";
e.currentTarget.style.backgroundColor = "#fff";
}}
title="Insert step"
>
<PlusIcon style={{ width: 14, height: 14, color: "#8c8c8c" }} />
<PlusIcon style={{ width: 10, height: 10, color: "#9ca3af" }} />
</button>
<div
style={{
width: 2,
flex: 1,
backgroundColor: "#d9d9d9",
}}
/>
<div style={{ width: 1, flex: 1, backgroundColor: "#e5e7eb" }} />
</div>
);
@ -116,52 +113,32 @@ const StepCard: React.FC<StepCardProps> = ({
return (
<div
style={{
border: "1px solid #e8e8e8",
borderRadius: 12,
padding: 20,
border: "1px solid #e5e7eb",
borderRadius: 6,
padding: "10px 14px",
backgroundColor: "#fff",
boxShadow: "0 1px 3px rgba(0,0,0,0.06)",
maxWidth: 680,
width: "100%",
}}
>
{/* Header */}
<div className="flex justify-between items-center" style={{ marginBottom: 16 }}>
<div className="flex items-center gap-2">
<Tag color="purple" style={{ margin: 0, fontWeight: 600, fontSize: 11 }}>
GUARDRAIL
</Tag>
<Text strong>{step.guardrail || "Select guardrail..."}</Text>
</div>
<div className="flex items-center gap-2">
<Text type="secondary" style={{ fontSize: 12 }}>
Step {stepIndex + 1}
</Text>
<button
onClick={onDelete}
disabled={totalSteps <= 1}
style={{
background: "none",
border: "none",
cursor: totalSteps <= 1 ? "not-allowed" : "pointer",
opacity: totalSteps <= 1 ? 0.3 : 1,
padding: 4,
}}
title="Delete step"
>
<TrashIcon style={{ width: 16, height: 16, color: "#ff4d4f" }} />
</button>
</div>
</div>
{/* Guardrail Selector */}
<div style={{ marginBottom: 12 }}>
<Text type="secondary" style={{ fontSize: 12, display: "block", marginBottom: 4 }}>
Guardrail
</Text>
{/* Row 1: GUARDRAIL label + selector + step number + delete */}
<div className="flex items-center gap-2" style={{ marginBottom: 8 }}>
<span
style={{
fontSize: 10,
fontWeight: 600,
textTransform: "uppercase",
color: "#9ca3af",
letterSpacing: "0.05em",
flexShrink: 0,
}}
>
GUARDRAIL
</span>
<Select
showSearch
style={{ width: "100%" }}
size="small"
style={{ flex: 1 }}
placeholder="Select a guardrail"
value={step.guardrail || undefined}
onChange={(value) => onChange({ guardrail: value })}
@ -170,66 +147,95 @@ const StepCard: React.FC<StepCardProps> = ({
(option?.label ?? "").toString().toLowerCase().includes(input.toLowerCase())
}
/>
<span style={{ fontSize: 11, color: "#9ca3af", flexShrink: 0 }}>
Step {stepIndex + 1}
</span>
<button
onClick={onDelete}
disabled={totalSteps <= 1}
style={{
background: "none",
border: "none",
cursor: totalSteps <= 1 ? "not-allowed" : "pointer",
opacity: totalSteps <= 1 ? 0.3 : 1,
padding: 2,
display: "flex",
alignItems: "center",
transition: "all 0.15s ease",
}}
onMouseEnter={(e) => {
if (totalSteps > 1) {
const icon = e.currentTarget.querySelector("svg");
if (icon) (icon as SVGElement).style.color = "#ef4444";
}
}}
onMouseLeave={(e) => {
const icon = e.currentTarget.querySelector("svg");
if (icon) (icon as SVGElement).style.color = "#9ca3af";
}}
title="Delete step"
>
<TrashIcon style={{ width: 14, height: 14, color: "#9ca3af", transition: "color 0.15s ease" }} />
</button>
</div>
{/* ON PASS */}
<div
style={{
backgroundColor: "#f6ffed",
border: "1px solid #b7eb8f",
borderRadius: 8,
padding: 12,
marginBottom: 8,
}}
>
<Text style={{ fontSize: 12, fontWeight: 600, color: "#52c41a" }}>
ON PASS
</Text>
<Select
style={{ width: "100%", marginTop: 4 }}
value={step.on_pass}
onChange={(value) => onChange({ on_pass: value as PipelineStep["on_pass"] })}
options={ACTION_OPTIONS}
/>
{step.on_pass === "modify_response" && (
<div style={{ marginTop: 8 }}>
<TextInput
placeholder="Custom response message"
value={step.modify_response_message || ""}
onChange={(e) => onChange({ modify_response_message: e.target.value || null })}
/>
</div>
)}
{/* Row 2: ON PASS + ON FAIL inline */}
<div className="flex items-center gap-4" style={{ flexWrap: "wrap" }}>
<div className="flex items-center gap-2" style={{ flex: 1, minWidth: 200 }}>
<span
style={{
fontSize: 10,
fontWeight: 600,
textTransform: "uppercase",
color: "#16a34a",
letterSpacing: "0.05em",
flexShrink: 0,
}}
>
ON PASS
</span>
<Select
size="small"
style={{ flex: 1 }}
value={step.on_pass}
onChange={(value) => onChange({ on_pass: value as PipelineStep["on_pass"] })}
options={ACTION_OPTIONS}
/>
</div>
<div className="flex items-center gap-2" style={{ flex: 1, minWidth: 200 }}>
<span
style={{
fontSize: 10,
fontWeight: 600,
textTransform: "uppercase",
color: "#dc2626",
letterSpacing: "0.05em",
flexShrink: 0,
}}
>
ON FAIL
</span>
<Select
size="small"
style={{ flex: 1 }}
value={step.on_fail}
onChange={(value) => onChange({ on_fail: value as PipelineStep["on_fail"] })}
options={ACTION_OPTIONS}
/>
</div>
</div>
{/* ON FAIL */}
<div
style={{
backgroundColor: "#fff2f0",
border: "1px solid #ffccc7",
borderRadius: 8,
padding: 12,
}}
>
<Text style={{ fontSize: 12, fontWeight: 600, color: "#ff4d4f" }}>
ON FAIL
</Text>
<Select
style={{ width: "100%", marginTop: 4 }}
value={step.on_fail}
onChange={(value) => onChange({ on_fail: value as PipelineStep["on_fail"] })}
options={ACTION_OPTIONS}
/>
{step.on_fail === "modify_response" && (
<div style={{ marginTop: 8 }}>
<TextInput
placeholder="Custom response message"
value={step.modify_response_message || ""}
onChange={(e) => onChange({ modify_response_message: e.target.value || null })}
/>
</div>
)}
</div>
{/* Custom response input (shown only when modify_response is selected) */}
{(step.on_pass === "modify_response" || step.on_fail === "modify_response") && (
<div style={{ marginTop: 6 }}>
<TextInput
placeholder="Custom response message"
value={step.modify_response_message || ""}
onChange={(e) => onChange({ modify_response_message: e.target.value || null })}
style={{ fontSize: 13 }}
/>
</div>
)}
</div>
);
};
@ -265,41 +271,37 @@ const PipelineFlowBuilder: React.FC<PipelineFlowBuilderProps> = ({
};
return (
<div className="flex flex-col items-center" style={{ padding: "16px 0" }}>
<div className="flex flex-col items-center" style={{ padding: "8px 0" }}>
{/* Trigger Card */}
<div
style={{
border: "1px solid #d9d9d9",
borderRadius: 12,
padding: "16px 24px",
border: "1px solid #e5e7eb",
borderRadius: 6,
padding: "10px 14px",
backgroundColor: "#fafafa",
maxWidth: 680,
width: "100%",
}}
>
<div className="flex items-center gap-3">
<div
style={{
width: 32,
height: 32,
borderRadius: "50%",
backgroundColor: "#f0f0f0",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span style={{ fontSize: 16 }}>&#9654;</span>
</div>
<div className="flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 24 24" fill="#9ca3af" stroke="none">
<polygon points="5,3 19,12 5,21" />
</svg>
<div>
<Text type="secondary" style={{ fontSize: 11, fontWeight: 600, display: "block" }}>
<span
style={{
fontSize: 10,
fontWeight: 600,
textTransform: "uppercase",
color: "#9ca3af",
letterSpacing: "0.05em",
}}
>
TRIGGER
</Text>
<Text strong>Incoming LLM Request</Text>
<br />
<Text type="secondary" style={{ fontSize: 12 }}>
This flow runs when a request matches this policy
</Text>
</span>
<span style={{ fontSize: 13, fontWeight: 500, marginLeft: 8 }}>
Incoming LLM Request
</span>
</div>
</div>
</div>
@ -329,13 +331,6 @@ const PipelineFlowBuilder: React.FC<PipelineFlowBuilderProps> = ({
// Read-only display for policy info view
// ─────────────────────────────────────────────────────────────────────────────
const ACTION_COLORS: Record<string, string> = {
allow: "green",
block: "red",
next: "blue",
modify_response: "orange",
};
const ACTION_LABELS: Record<string, string> = {
allow: "Allow",
block: "Block",
@ -348,67 +343,63 @@ interface PipelineInfoDisplayProps {
}
export const PipelineInfoDisplay: React.FC<PipelineInfoDisplayProps> = ({ pipeline }) => (
<div className="flex flex-col items-center" style={{ padding: "16px 0" }}>
<div className="flex flex-col items-center" style={{ padding: "8px 0" }}>
{/* Trigger */}
<div
style={{
border: "1px solid #d9d9d9",
borderRadius: 12,
padding: "12px 20px",
border: "1px solid #e5e7eb",
borderRadius: 6,
padding: "8px 14px",
backgroundColor: "#fafafa",
maxWidth: 680,
width: "100%",
}}
>
<div className="flex items-center gap-3">
<span style={{ fontSize: 16 }}>&#9654;</span>
<div>
<Text type="secondary" style={{ fontSize: 11, fontWeight: 600 }}>TRIGGER</Text>
<br />
<Text strong>Incoming LLM Request</Text>
</div>
<div className="flex items-center gap-2">
<svg width="12" height="12" viewBox="0 0 24 24" fill="#9ca3af" stroke="none">
<polygon points="5,3 19,12 5,21" />
</svg>
<span style={{ fontSize: 10, fontWeight: 600, textTransform: "uppercase", color: "#9ca3af", letterSpacing: "0.05em" }}>
TRIGGER
</span>
<span style={{ fontSize: 13, fontWeight: 500 }}>
Incoming LLM Request
</span>
</div>
</div>
{/* Steps */}
{pipeline.steps.map((step, index) => (
<React.Fragment key={index}>
{/* Connector line */}
<div style={{ width: 2, height: 32, backgroundColor: "#d9d9d9" }} />
{/* Step card */}
<div style={{ width: 1, height: 20, backgroundColor: "#e5e7eb" }} />
<div
style={{
border: "1px solid #e8e8e8",
borderRadius: 12,
padding: "12px 20px",
border: "1px solid #e5e7eb",
borderRadius: 6,
padding: "8px 14px",
backgroundColor: "#fff",
maxWidth: 680,
width: "100%",
}}
>
<div className="flex justify-between items-center" style={{ marginBottom: 8 }}>
<div className="flex items-center gap-2">
<Tag color="purple" style={{ margin: 0, fontSize: 11, fontWeight: 600 }}>
GUARDRAIL
</Tag>
<Text strong>{step.guardrail}</Text>
</div>
<Text type="secondary" style={{ fontSize: 12 }}>Step {index + 1}</Text>
<div className="flex items-center gap-2" style={{ marginBottom: 4 }}>
<span style={{ fontSize: 10, fontWeight: 600, textTransform: "uppercase", color: "#9ca3af", letterSpacing: "0.05em" }}>
GUARDRAIL
</span>
<span style={{ fontSize: 13, fontWeight: 500 }}>{step.guardrail}</span>
<span style={{ fontSize: 11, color: "#9ca3af", marginLeft: "auto" }}>Step {index + 1}</span>
</div>
<div className="flex gap-3">
<Text type="secondary" style={{ fontSize: 12 }}>
Pass →{" "}
<Tag color={ACTION_COLORS[step.on_pass]} style={{ fontSize: 11 }}>
{ACTION_LABELS[step.on_pass] || step.on_pass}
</Tag>
</Text>
<Text type="secondary" style={{ fontSize: 12 }}>
Fail →{" "}
<Tag color={ACTION_COLORS[step.on_fail]} style={{ fontSize: 11 }}>
{ACTION_LABELS[step.on_fail] || step.on_fail}
</Tag>
</Text>
<div className="flex gap-4" style={{ fontSize: 12, color: "#6b7280" }}>
<span>
<span style={{ fontSize: 10, fontWeight: 600, color: "#16a34a", textTransform: "uppercase" }}>Pass</span>
{" "}
{ACTION_LABELS[step.on_pass] || step.on_pass}
</span>
<span>
<span style={{ fontSize: 10, fontWeight: 600, color: "#dc2626", textTransform: "uppercase" }}>Fail</span>
{" "}
{ACTION_LABELS[step.on_fail] || step.on_fail}
</span>
</div>
</div>
</React.Fragment>
@ -506,7 +497,7 @@ export const FlowBuilderPage: React.FC<FlowBuilderPageProps> = ({
left: 0,
right: 0,
bottom: 0,
backgroundColor: "#f8f9fa",
backgroundColor: "#fafafa",
zIndex: 1000,
display: "flex",
flexDirection: "column",
@ -518,14 +509,14 @@ export const FlowBuilderPage: React.FC<FlowBuilderPageProps> = ({
style={{
borderBottom: "1px solid #e5e7eb",
backgroundColor: "#fff",
padding: "12px 24px",
padding: "10px 24px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexShrink: 0,
}}
>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<button
onClick={onBack}
style={{
@ -537,33 +528,40 @@ export const FlowBuilderPage: React.FC<FlowBuilderPageProps> = ({
alignItems: "center",
}}
>
<ArrowLeftIcon style={{ width: 20, height: 20, color: "#6b7280" }} />
<ArrowLeftIcon style={{ width: 16, height: 16, color: "#9ca3af" }} />
</button>
<Text type="secondary">Policies</Text>
<Text type="secondary">/</Text>
<span style={{ fontSize: 13, color: "#9ca3af" }}>Policies</span>
<span style={{ fontSize: 13, color: "#d1d5db" }}>/</span>
<TextInput
placeholder="Policy name..."
value={policyName}
onChange={(e) => setPolicyName(e.target.value)}
disabled={isEditing}
style={{ width: 240 }}
style={{ width: 220, fontSize: 13 }}
/>
<Tag color="purple" style={{ margin: 0, fontSize: 11, fontWeight: 600 }}>
<span
style={{
fontSize: 10,
fontWeight: 600,
backgroundColor: "#f3f4f6",
color: "#6b7280",
padding: "2px 6px",
borderRadius: 3,
textTransform: "uppercase",
letterSpacing: "0.05em",
}}
>
Flow
</Tag>
</span>
</div>
<div className="flex items-center gap-2">
<Button variant="secondary" onClick={onBack}>
<Button variant="secondary" onClick={onBack} style={{ fontSize: 13 }}>
Cancel
</Button>
<Button
onClick={handleSave}
loading={isSubmitting}
style={{
backgroundColor: "#4f46e5",
color: "#fff",
border: "none",
}}
style={{ fontSize: 13 }}
>
{isEditing ? "Update Policy" : "Save Policy"}
</Button>
@ -573,7 +571,7 @@ export const FlowBuilderPage: React.FC<FlowBuilderPageProps> = ({
{/* Description bar */}
<div
style={{
padding: "8px 24px",
padding: "6px 24px",
backgroundColor: "#fff",
borderBottom: "1px solid #e5e7eb",
flexShrink: 0,
@ -583,21 +581,21 @@ export const FlowBuilderPage: React.FC<FlowBuilderPageProps> = ({
placeholder="Add a description (optional)..."
value={description}
onChange={(e) => setDescription(e.target.value)}
style={{ maxWidth: 500 }}
style={{ maxWidth: 480, fontSize: 13 }}
/>
</div>
{/* Flow builder canvas - scrollable */}
{/* Flow builder canvas */}
<div
style={{
flex: 1,
overflowY: "auto",
display: "flex",
justifyContent: "center",
padding: "40px 24px",
padding: "24px 24px",
}}
>
<div style={{ maxWidth: 760, width: "100%" }}>
<div style={{ maxWidth: 720, width: "100%" }}>
<PipelineFlowBuilder
pipeline={pipeline}
onChange={setPipeline}