mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
feat(ui): add test playground to AI policy suggestion modal
This commit is contained in:
parent
003cf2e977
commit
775490bc90
1 changed files with 485 additions and 137 deletions
|
|
@ -1,13 +1,24 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Modal, Spin, Checkbox, Select } from "antd";
|
||||
import { Button } from "@tremor/react";
|
||||
import { suggestPolicyTemplates, modelHubCall } from "../networking";
|
||||
import { Modal, Spin, Checkbox, Select, Input, Typography, Tooltip } from "antd";
|
||||
import { Button, Card } from "@tremor/react";
|
||||
import { CheckCircleOutlined, CloseCircleOutlined, InfoCircleOutlined, DownOutlined, RightOutlined } from "@ant-design/icons";
|
||||
import { suggestPolicyTemplates, modelHubCall, testPolicyTemplate, enrichPolicyTemplate } from "../networking";
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Text } = Typography;
|
||||
|
||||
interface SuggestedTemplate {
|
||||
template_id: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
interface GuardrailTestResult {
|
||||
guardrail_name: string;
|
||||
action: string;
|
||||
output_text: string;
|
||||
details: string;
|
||||
}
|
||||
|
||||
interface AiSuggestionModalProps {
|
||||
visible: boolean;
|
||||
onSelectTemplates: (templates: any[]) => void;
|
||||
|
|
@ -34,6 +45,17 @@ const AiSuggestionModal: React.FC<AiSuggestionModalProps> = ({
|
|||
const [selectedModel, setSelectedModel] = useState<string | undefined>(undefined);
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
||||
// Test panel state
|
||||
const [showTestPanel, setShowTestPanel] = useState(false);
|
||||
const [testInputText, setTestInputText] = useState("");
|
||||
const [isTestLoading, setIsTestLoading] = useState(false);
|
||||
const [testResults, setTestResults] = useState<GuardrailTestResult[] | null>(null);
|
||||
const [testOverallAction, setTestOverallAction] = useState<string | null>(null);
|
||||
const [collapsedResults, setCollapsedResults] = useState<Set<string>>(new Set());
|
||||
// Enrichment state for competitor templates
|
||||
const [enrichedDefs, setEnrichedDefs] = useState<Record<string, any[]>>({});
|
||||
const [isEnriching, setIsEnriching] = useState(false);
|
||||
const [enrichBrandName, setEnrichBrandName] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && availableModels.length === 0) {
|
||||
|
|
@ -67,6 +89,15 @@ const AiSuggestionModal: React.FC<AiSuggestionModalProps> = ({
|
|||
setExplanation(null);
|
||||
setSelectedIds(new Set());
|
||||
setSelectedModel(undefined);
|
||||
setShowTestPanel(false);
|
||||
setTestInputText("");
|
||||
setIsTestLoading(false);
|
||||
setTestResults(null);
|
||||
setTestOverallAction(null);
|
||||
setCollapsedResults(new Set());
|
||||
setEnrichedDefs({});
|
||||
setIsEnriching(false);
|
||||
setEnrichBrandName("");
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
|
|
@ -126,6 +157,11 @@ const AiSuggestionModal: React.FC<AiSuggestionModalProps> = ({
|
|||
setSuggestions(null);
|
||||
setExplanation(null);
|
||||
setSelectedIds(new Set());
|
||||
setShowTestPanel(false);
|
||||
setTestInputText("");
|
||||
setTestResults(null);
|
||||
setTestOverallAction(null);
|
||||
setCollapsedResults(new Set());
|
||||
};
|
||||
|
||||
const handleUseSelected = () => {
|
||||
|
|
@ -149,14 +185,433 @@ const AiSuggestionModal: React.FC<AiSuggestionModalProps> = ({
|
|||
const getTemplateById = (id: string) =>
|
||||
allTemplates.find((t) => t.id === id);
|
||||
|
||||
const toggleResultCollapse = (name: string) => {
|
||||
setCollapsedResults((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(name)) next.delete(name);
|
||||
else next.add(name);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const getSelectedTemplatesNeedingEnrichment = (): any[] => {
|
||||
return Array.from(selectedIds)
|
||||
.map((id) => getTemplateById(id))
|
||||
.filter((t) => t?.llm_enrichment);
|
||||
};
|
||||
|
||||
const needsEnrichment = getSelectedTemplatesNeedingEnrichment().length > 0;
|
||||
|
||||
const getAllSelectedGuardrailDefs = (): any[] => {
|
||||
const defs: any[] = [];
|
||||
for (const id of selectedIds) {
|
||||
// Use enriched defs if available, otherwise use template's original
|
||||
if (enrichedDefs[id]) {
|
||||
defs.push(...enrichedDefs[id]);
|
||||
} else {
|
||||
const t = getTemplateById(id);
|
||||
if (t?.guardrailDefinitions) {
|
||||
defs.push(...t.guardrailDefinitions);
|
||||
}
|
||||
}
|
||||
}
|
||||
return defs;
|
||||
};
|
||||
|
||||
const handleEnrichCompetitors = async () => {
|
||||
if (!accessToken || !selectedModel) return;
|
||||
const templatesToEnrich = getSelectedTemplatesNeedingEnrichment();
|
||||
if (templatesToEnrich.length === 0) return;
|
||||
|
||||
setIsEnriching(true);
|
||||
try {
|
||||
for (const template of templatesToEnrich) {
|
||||
const paramName = template.llm_enrichment.parameter;
|
||||
const result = await enrichPolicyTemplate(
|
||||
accessToken,
|
||||
template.id,
|
||||
{ [paramName]: enrichBrandName },
|
||||
selectedModel,
|
||||
);
|
||||
setEnrichedDefs((prev) => ({
|
||||
...prev,
|
||||
[template.id]: result.guardrailDefinitions,
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to enrich templates:", e);
|
||||
} finally {
|
||||
setIsEnriching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRunTest = async () => {
|
||||
if (!accessToken || !testInputText.trim()) return;
|
||||
const allDefs = getAllSelectedGuardrailDefs();
|
||||
if (allDefs.length === 0) return;
|
||||
|
||||
setIsTestLoading(true);
|
||||
setTestResults(null);
|
||||
setTestOverallAction(null);
|
||||
setCollapsedResults(new Set());
|
||||
|
||||
try {
|
||||
const result = await testPolicyTemplate(accessToken, allDefs, testInputText);
|
||||
setTestResults(result.results || []);
|
||||
setTestOverallAction(result.overall_action || "passed");
|
||||
} catch {
|
||||
setTestResults([]);
|
||||
setTestOverallAction("error");
|
||||
} finally {
|
||||
setIsTestLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
||||
e.preventDefault();
|
||||
handleRunTest();
|
||||
}
|
||||
};
|
||||
|
||||
const showResults = suggestions !== null && !isLoading;
|
||||
|
||||
// Helper to render the suggestions list (reused in both layouts)
|
||||
const renderSuggestionsList = () => {
|
||||
if (!suggestions || suggestions.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
<svg className="w-12 h-12 mx-auto mb-3 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p className="font-medium">No matching templates found</p>
|
||||
<p className="text-sm mt-1">Try adjusting your examples or description.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{suggestions.map((suggestion) => {
|
||||
const template = getTemplateById(suggestion.template_id);
|
||||
if (!template) return null;
|
||||
const isSelected = selectedIds.has(suggestion.template_id);
|
||||
return (
|
||||
<div
|
||||
key={suggestion.template_id}
|
||||
className={`rounded-xl border-2 transition-all ${
|
||||
isSelected
|
||||
? "border-blue-400 bg-blue-50/60 shadow-sm"
|
||||
: "border-gray-200 hover:border-gray-300 hover:shadow-sm"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="p-4 cursor-pointer"
|
||||
onClick={() => toggleTemplate(suggestion.template_id)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={() => toggleTemplate(suggestion.template_id)}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-semibold text-sm text-gray-900">
|
||||
{template.title}
|
||||
</span>
|
||||
{template.complexity && (
|
||||
<span className={`px-2 py-0.5 rounded-full text-[10px] font-medium border ${
|
||||
template.complexity === "Low"
|
||||
? "bg-gray-50 text-gray-500 border-gray-200"
|
||||
: template.complexity === "Medium"
|
||||
? "bg-blue-50 text-blue-500 border-blue-100"
|
||||
: "bg-purple-50 text-purple-500 border-purple-100"
|
||||
}`}>
|
||||
{template.complexity}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 leading-relaxed">
|
||||
{template.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-1.5 mt-2">
|
||||
{template.guardrails && template.guardrails.slice(0, 4).map((g: string) => (
|
||||
<span key={g} className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-gray-100 text-gray-600">
|
||||
{g}
|
||||
</span>
|
||||
))}
|
||||
{template.guardrails && template.guardrails.length > 4 && (
|
||||
<span className="text-[10px] text-gray-400">
|
||||
+{template.guardrails.length - 4} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 flex items-start gap-1.5">
|
||||
<InfoCircleOutlined className="text-blue-500 mt-0.5 text-xs flex-shrink-0" />
|
||||
<p className="text-xs text-blue-600 leading-relaxed">
|
||||
{suggestion.reason}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Explanation */}
|
||||
{explanation && (
|
||||
<div className="p-3 bg-gray-50 rounded-xl border border-gray-200">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<InfoCircleOutlined className="text-gray-400 text-xs" />
|
||||
<span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">
|
||||
Why these templates
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 leading-relaxed">{explanation}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Helper to render the test panel
|
||||
const renderTestPanel = () => (
|
||||
<div className="space-y-4 h-full flex flex-col">
|
||||
{/* Test header */}
|
||||
<div className="pb-3 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<h3 className="text-base font-semibold text-gray-900">Test Guardrails</h3>
|
||||
<button
|
||||
onClick={() => { setShowTestPanel(false); setTestResults(null); setTestOverallAction(null); }}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 mb-1.5">
|
||||
{Array.from(selectedIds).map((id) => {
|
||||
const t = getTemplateById(id);
|
||||
return t ? (
|
||||
<span key={id} className="inline-flex items-center px-2 py-0.5 rounded-md text-[10px] font-medium bg-blue-50 text-blue-700 border border-blue-200">
|
||||
{t.title}
|
||||
</span>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
{getAllSelectedGuardrailDefs().length} guardrails across {selectedIds.size} template{selectedIds.size !== 1 ? "s" : ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Enrichment section for competitor templates */}
|
||||
{needsEnrichment && Object.keys(enrichedDefs).length > 0 && (
|
||||
<div className="p-3 bg-green-50 rounded-lg border border-green-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircleOutlined className="text-green-600" />
|
||||
<span className="text-xs font-medium text-green-800">
|
||||
Competitor names loaded for {enrichBrandName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{needsEnrichment && Object.keys(enrichedDefs).length === 0 && (
|
||||
<div className="p-3 bg-amber-50 rounded-lg border border-amber-200 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-amber-600 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span className="text-xs font-medium text-amber-800">
|
||||
Competitor template requires your brand name to discover competitors
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
size="small"
|
||||
placeholder="e.g. Emirates Airlines"
|
||||
value={enrichBrandName}
|
||||
onChange={(e) => setEnrichBrandName(e.target.value)}
|
||||
onPressEnter={() => enrichBrandName.trim() && handleEnrichCompetitors()}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
onClick={handleEnrichCompetitors}
|
||||
loading={isEnriching}
|
||||
disabled={!enrichBrandName.trim() || isEnriching}
|
||||
>
|
||||
{isEnriching ? "Discovering..." : "Discover"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input */}
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Input Text</label>
|
||||
<Tooltip title="Press Enter to submit. Use Shift+Enter for new line.">
|
||||
<InfoCircleOutlined className="text-gray-400 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Text className="text-xs text-gray-500">Characters: {testInputText.length}</Text>
|
||||
</div>
|
||||
<TextArea
|
||||
value={testInputText}
|
||||
onChange={(e) => setTestInputText(e.target.value)}
|
||||
onKeyDown={handleTestKeyDown}
|
||||
placeholder="Enter text to test against all selected policy guardrails..."
|
||||
rows={4}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<div className="mt-1">
|
||||
<Text className="text-xs text-gray-500">
|
||||
Press <kbd className="px-1 py-0.5 bg-gray-100 border border-gray-300 rounded text-xs">Enter</kbd> to submit
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleRunTest}
|
||||
loading={isTestLoading}
|
||||
disabled={!testInputText.trim() || isTestLoading}
|
||||
className="w-full"
|
||||
>
|
||||
{isTestLoading
|
||||
? `Testing ${getAllSelectedGuardrailDefs().length} guardrails...`
|
||||
: `Test ${getAllSelectedGuardrailDefs().length} guardrails`}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{testResults && testResults.length > 0 && (() => {
|
||||
const blockedCount = testResults.filter((r) => r.action === "blocked").length;
|
||||
const maskedCount = testResults.filter((r) => r.action === "masked").length;
|
||||
const passedCount = testResults.filter((r) => r.action === "passed").length;
|
||||
const otherCount = testResults.length - blockedCount - maskedCount - passedCount;
|
||||
return (
|
||||
<div className="space-y-2 pt-3 border-t border-gray-200 flex-1 overflow-y-auto">
|
||||
{/* Summary bar */}
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-3 mb-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-sm font-semibold text-gray-900">Results</h4>
|
||||
<span className="text-[10px] text-gray-500">{testResults.length} guardrails tested</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{blockedCount > 0 && (
|
||||
<div className="flex-1 rounded-md bg-red-50 border border-red-200 px-3 py-2 text-center">
|
||||
<div className="text-lg font-bold text-red-700">{blockedCount}</div>
|
||||
<div className="text-[10px] font-medium text-red-600">Blocked</div>
|
||||
</div>
|
||||
)}
|
||||
{maskedCount > 0 && (
|
||||
<div className="flex-1 rounded-md bg-amber-50 border border-amber-200 px-3 py-2 text-center">
|
||||
<div className="text-lg font-bold text-amber-700">{maskedCount}</div>
|
||||
<div className="text-[10px] font-medium text-amber-600">Masked</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 rounded-md bg-green-50 border border-green-200 px-3 py-2 text-center">
|
||||
<div className="text-lg font-bold text-green-700">{passedCount}</div>
|
||||
<div className="text-[10px] font-medium text-green-600">Passed</div>
|
||||
</div>
|
||||
{otherCount > 0 && (
|
||||
<div className="flex-1 rounded-md bg-gray-100 border border-gray-200 px-3 py-2 text-center">
|
||||
<div className="text-lg font-bold text-gray-600">{otherCount}</div>
|
||||
<div className="text-[10px] font-medium text-gray-500">Other</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{testResults.map((result) => {
|
||||
const isBlocked = result.action === "blocked";
|
||||
const isMasked = result.action === "masked";
|
||||
const isPassed = result.action === "passed";
|
||||
const isCollapsed = collapsedResults.has(result.guardrail_name);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={result.guardrail_name}
|
||||
className={`!p-3 ${
|
||||
isBlocked
|
||||
? "bg-red-50 border-red-200"
|
||||
: isMasked
|
||||
? "bg-amber-50 border-amber-200"
|
||||
: isPassed
|
||||
? "bg-green-50 border-green-200"
|
||||
: "bg-gray-50 border-gray-200"
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer"
|
||||
onClick={() => toggleResultCollapse(result.guardrail_name)}
|
||||
>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
{isCollapsed ? <RightOutlined className="text-gray-500 text-[10px]" /> : <DownOutlined className="text-gray-500 text-[10px]" />}
|
||||
{isBlocked ? (
|
||||
<CloseCircleOutlined className="text-red-600" />
|
||||
) : isMasked ? (
|
||||
<svg className="w-4 h-4 text-amber-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<CheckCircleOutlined className="text-green-600" />
|
||||
)}
|
||||
<span className={`text-xs font-medium ${isBlocked ? "text-red-800" : isMasked ? "text-amber-800" : "text-green-800"}`}>
|
||||
{result.guardrail_name}
|
||||
</span>
|
||||
<span className={`px-1.5 py-0.5 rounded-full text-[10px] font-semibold ${
|
||||
isBlocked ? "bg-red-100 text-red-700" : isMasked ? "bg-amber-100 text-amber-700" : isPassed ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-600"
|
||||
}`}>
|
||||
{result.action.charAt(0).toUpperCase() + result.action.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
{isMasked && result.output_text && (
|
||||
<div className="bg-white border border-amber-200 rounded p-2">
|
||||
<label className="text-[10px] font-medium text-gray-600 mb-1 block">Output Text</label>
|
||||
<div className="font-mono text-xs text-gray-900 whitespace-pre-wrap break-words">{result.output_text}</div>
|
||||
</div>
|
||||
)}
|
||||
{isBlocked && result.details && (
|
||||
<div className="bg-white border border-red-200 rounded p-2">
|
||||
<label className="text-[10px] font-medium text-gray-600 mb-1 block">Details</label>
|
||||
<p className="text-xs text-red-700">{result.details}</p>
|
||||
</div>
|
||||
)}
|
||||
{isPassed && (
|
||||
<div className="text-[10px] text-green-700">Passed unchanged.</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{testResults && testResults.length === 0 && !isTestLoading && (
|
||||
<p className="text-xs text-gray-400 text-center py-3">No testable guardrails in selected templates.</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={null}
|
||||
open={visible}
|
||||
onCancel={handleCancel}
|
||||
width={820}
|
||||
width={showTestPanel ? 1200 : 820}
|
||||
footer={null}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
|
|
@ -290,26 +745,14 @@ const AiSuggestionModal: React.FC<AiSuggestionModalProps> = ({
|
|||
{isLoading && (
|
||||
<div className="flex items-center justify-center gap-3 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<Spin size="small" />
|
||||
<span className="text-sm text-gray-600">
|
||||
Analyzing your requirements...
|
||||
</span>
|
||||
<span className="text-sm text-gray-600">Analyzing your requirements...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleCancel}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSuggest}
|
||||
loading={isLoading}
|
||||
disabled={!hasInput || !selectedModel || isLoading}
|
||||
>
|
||||
<Button variant="secondary" onClick={handleCancel} disabled={isLoading}>Cancel</Button>
|
||||
<Button onClick={handleSuggest} loading={isLoading} disabled={!hasInput || !selectedModel || isLoading}>
|
||||
{isLoading ? "Analyzing..." : "Suggest Policies"}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -317,130 +760,35 @@ const AiSuggestionModal: React.FC<AiSuggestionModalProps> = ({
|
|||
) : (
|
||||
/* ── Results phase ── */
|
||||
<div className="px-8 py-6">
|
||||
{suggestions && suggestions.length > 0 ? (
|
||||
<div className="space-y-3 max-h-[450px] overflow-y-auto pr-1">
|
||||
{suggestions.map((suggestion) => {
|
||||
const template = getTemplateById(suggestion.template_id);
|
||||
if (!template) return null;
|
||||
const isSelected = selectedIds.has(suggestion.template_id);
|
||||
return (
|
||||
<div
|
||||
key={suggestion.template_id}
|
||||
className={`p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
||||
isSelected
|
||||
? "border-blue-400 bg-blue-50/60 shadow-sm"
|
||||
: "border-gray-200 hover:border-gray-300 hover:shadow-sm"
|
||||
}`}
|
||||
onClick={() => toggleTemplate(suggestion.template_id)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={() => toggleTemplate(suggestion.template_id)}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-semibold text-sm text-gray-900">
|
||||
{template.title}
|
||||
</span>
|
||||
{template.complexity && (
|
||||
<span className={`px-2 py-0.5 rounded-full text-[10px] font-medium border ${
|
||||
template.complexity === "Low"
|
||||
? "bg-gray-50 text-gray-500 border-gray-200"
|
||||
: template.complexity === "Medium"
|
||||
? "bg-blue-50 text-blue-500 border-blue-100"
|
||||
: "bg-purple-50 text-purple-500 border-purple-100"
|
||||
}`}>
|
||||
{template.complexity}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 leading-relaxed">
|
||||
{template.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-1.5 mt-2">
|
||||
{template.guardrails && template.guardrails.slice(0, 4).map((g: string) => (
|
||||
<span key={g} className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-gray-100 text-gray-600">
|
||||
{g}
|
||||
</span>
|
||||
))}
|
||||
{template.guardrails && template.guardrails.length > 4 && (
|
||||
<span className="text-[10px] text-gray-400">
|
||||
+{template.guardrails.length - 4} more
|
||||
</span>
|
||||
)}
|
||||
{template.estimated_latency && (
|
||||
<>
|
||||
<span className="text-gray-300">|</span>
|
||||
<span className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium ${
|
||||
template.estimated_latency.includes("<1ms")
|
||||
? "bg-green-50 text-green-600"
|
||||
: "bg-amber-50 text-amber-600"
|
||||
}`}>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{template.estimated_latency}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2.5 flex items-start gap-1.5">
|
||||
<svg className="w-3.5 h-3.5 text-blue-500 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<p className="text-xs text-blue-600 leading-relaxed">
|
||||
{suggestion.reason}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{showTestPanel && selectedIds.size > 0 ? (
|
||||
/* Side-by-side layout: suggestions left, test panel right */
|
||||
<div className="flex gap-6" style={{ minHeight: "500px", maxHeight: "70vh" }}>
|
||||
{/* Left: suggestions */}
|
||||
<div className="w-1/2 overflow-y-auto pr-2">
|
||||
{renderSuggestionsList()}
|
||||
</div>
|
||||
{/* Right: test panel */}
|
||||
<div className="w-1/2 border-l border-gray-200 pl-6 overflow-y-auto">
|
||||
{renderTestPanel()}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
<svg className="w-12 h-12 mx-auto mb-3 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p className="font-medium">No matching templates found</p>
|
||||
<p className="text-sm mt-1">
|
||||
Try adjusting your examples or description.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Explanation */}
|
||||
{explanation && suggestions && suggestions.length > 0 && (
|
||||
<div className="mt-4 p-4 bg-gray-50 rounded-xl border border-gray-200">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||
Why these templates
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 leading-relaxed">{explanation}</p>
|
||||
/* Normal single-column layout */
|
||||
<div className="max-h-[520px] overflow-y-auto pr-1">
|
||||
{renderSuggestionsList()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end gap-3 pt-6">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleBack}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleUseSelected}
|
||||
disabled={selectedIds.size === 0}
|
||||
>
|
||||
Use {selectedIds.size} Selected Template
|
||||
{selectedIds.size !== 1 ? "s" : ""}
|
||||
<div className="flex justify-end gap-3 pt-6 border-t border-gray-100 mt-4">
|
||||
<Button variant="secondary" onClick={handleBack}>Back</Button>
|
||||
{suggestions && suggestions.length > 0 && selectedIds.size > 0 && !showTestPanel && (
|
||||
<Button variant="secondary" onClick={() => setShowTestPanel(true)}>
|
||||
Test Suggestions
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleUseSelected} disabled={selectedIds.size === 0}>
|
||||
Use {selectedIds.size} Selected Template{selectedIds.size !== 1 ? "s" : ""}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue