mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
refactor(ui): move the agent, guardrail, prompt, policy and skill forms off tremor (#37320)
* refactor(ui): move the agent, guardrail, prompt, policy and skill forms off tremor The agent info Save Changes button used to rely on tremor's implicit submit inside the antd Form, so it now carries an explicit type="submit". Every converted TabsContent is keepMounted to keep tremor's always-mounted panel semantics, pinned by a new guardrail info test. Prunes the tremor no-restricted-imports suppressions these nine files no longer need. * fix(ui): keep the line tab strip on the agent, guardrail and prompt info views tremor's TabList defaulted to the line variant while shadcn's TabsList defaults to the filled pill, so the bare conversion turned three underlined tab strips into segmented pills. Restores the line variant plus the bottom border and the tab padding the strips used to have. * test(ui): pin the agent settings submit and the prompt raw json tab Agent Settings only saves because Save Changes carries an explicit type="submit" now that the button is a Base UI button, so a test drives the edit and asserts the patch call fires. The prompt info tabs are keyed by slug now, which also makes Raw JSON render for prompts with no template, so a second test renders that case and asserts the serialized response is visible.
This commit is contained in:
parent
589f6859d8
commit
669c1334b4
12 changed files with 373 additions and 303 deletions
|
|
@ -4,11 +4,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/access-groups/_components/AccessGroupsModal/AccessGroupBaseForm.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/access-groups/_components/AccessGroupsModal/AccessGroupEditModal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
|
@ -33,7 +28,7 @@
|
|||
"count": 3
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 2
|
||||
|
|
@ -58,12 +53,6 @@
|
|||
"src/app/(dashboard)/agents/_components/agent_form_fields.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
},
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/agents/_components/agent_info.tsx": {
|
||||
|
|
@ -73,11 +62,8 @@
|
|||
"local/no-complex-jsx-arrow": {
|
||||
"count": 1
|
||||
},
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/immutability": {
|
||||
"count": 1
|
||||
|
|
@ -94,20 +80,11 @@
|
|||
"src/app/(dashboard)/agents/_components/cost_config_fields.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/agents/_components/dynamic_agent_form_fields.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
},
|
||||
"no-nested-ternary": {
|
||||
"count": 2
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/budgets/_components/budget_modal.tsx": {
|
||||
|
|
@ -115,7 +92,7 @@
|
|||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/budgets/_components/budget_panel.tsx": {
|
||||
|
|
@ -128,7 +105,7 @@
|
|||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/caching/_components/cache_dashboard.tsx": {
|
||||
|
|
@ -147,16 +124,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/caching/_components/cache_settings/CacheFormField.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/caching/_components/cache_settings/cacheSettingsFields.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/caching/_components/cache_settings/index.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
|
|
@ -165,22 +132,9 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/caching/_components/coordination_redis_settings/CoordinationRedisFormField.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/caching/_components/coordination_redis_settings/coordinationRedisFields.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/caching/_components/coordination_redis_settings/index.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/caching/_components/response_time_indicator.tsx": {
|
||||
|
|
@ -383,7 +337,7 @@
|
|||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 3
|
||||
|
|
@ -1204,18 +1158,7 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/projects/_components/ProjectModals/ProjectBaseForm.test.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/projects/_components/ProjectModals/ProjectBaseForm.tsx": {
|
||||
"local/no-complex-jsx-arrow": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
|
|
@ -1286,7 +1229,7 @@
|
|||
"count": 3
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 2
|
||||
|
|
@ -1354,12 +1297,12 @@
|
|||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/tag-management/_components/components/CreateTagModal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/tag-management/_components/index.tsx": {
|
||||
|
|
@ -1475,9 +1418,6 @@
|
|||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
},
|
||||
"local/no-complex-jsx-arrow": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
|
|
@ -1645,11 +1585,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/SSOModals.test.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/SSOModals.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
|
@ -1691,11 +1626,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/Settings/AdminSettings/SSOSettings/Modals/BaseSSOSettingsForm.test.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/Settings/AdminSettings/SSOSettings/Modals/EditSSOSettingsModal.test.tsx": {
|
||||
"max-nested-callbacks": {
|
||||
"count": 1
|
||||
|
|
@ -2098,14 +2028,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/common_components/KeyLifecycleSettings.tsx": {
|
||||
"local/no-complex-jsx-arrow": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/common_components/MetadataKeyValueFields.test.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
|
@ -2136,11 +2058,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/common_components/RateLimitTypeFormItem.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/common_components/budget_duration_dropdown.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
|
|
@ -3150,4 +3067,4 @@
|
|||
"count": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@ import { Modal, Select, Steps, Tag } from "antd";
|
|||
import { FormProvider, useForm, useWatch } from "react-hook-form";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { Logo } from "@/components/molecules/logo/Logo";
|
||||
import { Button } from "@tremor/react";
|
||||
import { CheckCircleFilled, KeyOutlined, RobotOutlined, AppstoreOutlined } from "@ant-design/icons";
|
||||
import CreatedKeyDisplay from "@/components/shared/CreatedKeyDisplay";
|
||||
import { Button as ShadButton } from "@/components/ui/button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
|
@ -1025,9 +1025,9 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
|
|||
<div className="mt-6 flex items-center justify-between border-t border-border pt-6">
|
||||
<div>
|
||||
{currentStep > 0 && currentStep < 4 && (
|
||||
<ShadButton type="button" variant="outline" onClick={handleBack}>
|
||||
<Button type="button" variant="outline" onClick={handleBack}>
|
||||
← Back
|
||||
</ShadButton>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
|
|
@ -1036,21 +1036,14 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
|
|||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
{currentStep < 3 && (
|
||||
<Button variant="primary" onClick={handleNext}>
|
||||
Next →
|
||||
</Button>
|
||||
)}
|
||||
{currentStep < 3 && <Button onClick={handleNext}>Next →</Button>}
|
||||
{currentStep === 3 && (
|
||||
<Button variant="primary" loading={isSubmitting} onClick={handleCreateAgent}>
|
||||
<Button disabled={isSubmitting} aria-busy={isSubmitting} onClick={handleCreateAgent}>
|
||||
{isSubmitting && <UiLoadingSpinner className="size-4" />}
|
||||
{isSubmitting ? "Creating..." : "Create Agent →"}
|
||||
</Button>
|
||||
)}
|
||||
{currentStep === 4 && (
|
||||
<Button variant="primary" onClick={handleClose}>
|
||||
Done
|
||||
</Button>
|
||||
)}
|
||||
{currentStep === 4 && <Button onClick={handleClose}>Done</Button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
import React from "react";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import AgentInfoView from "./agent_info";
|
||||
import * as networking from "@/components/networking";
|
||||
import type { Agent } from "@/components/agents/types";
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
getAgentInfo: vi.fn(),
|
||||
getAgentCreateMetadata: vi.fn(),
|
||||
patchAgentCall: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/keys/useKeys", () => ({
|
||||
useKeys: () => ({ data: { keys: [] }, isLoading: false, refetch: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock("./agent_card_discovery", () => ({
|
||||
default: () => <div data-testid="agent-card-discovery" />,
|
||||
}));
|
||||
|
||||
vi.mock("./agent_form_fields", () => ({
|
||||
default: () => <div data-testid="agent-form-fields" />,
|
||||
unmountedA2AFieldNames: () => [],
|
||||
}));
|
||||
|
||||
const agent = {
|
||||
agent_id: "agent-1",
|
||||
agent_name: "support-agent",
|
||||
agent_card_params: {
|
||||
name: "Support Agent",
|
||||
description: "Answers support questions",
|
||||
url: "http://localhost:9999/",
|
||||
version: "1.0.0",
|
||||
protocolVersion: "1.0",
|
||||
capabilities: { streaming: false },
|
||||
skills: [],
|
||||
},
|
||||
tpm_limit: 100,
|
||||
} as unknown as Agent;
|
||||
|
||||
describe("AgentInfoView settings", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(networking.getAgentInfo).mockReset().mockResolvedValue(agent);
|
||||
vi.mocked(networking.getAgentCreateMetadata).mockReset().mockResolvedValue([]);
|
||||
vi.mocked(networking.patchAgentCall).mockReset().mockResolvedValue({});
|
||||
});
|
||||
|
||||
it("submits the edited agent when Save Changes is pressed", async () => {
|
||||
render(<AgentInfoView agentId="agent-1" onClose={vi.fn()} accessToken="sk-test" isAdmin={true} />);
|
||||
|
||||
fireEvent.click(await screen.findByRole("tab", { name: "Settings" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Edit Settings" }));
|
||||
|
||||
const tpmLimit = await screen.findByLabelText("TPM Limit");
|
||||
fireEvent.change(tpmLimit, { target: { value: "42" } });
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Save Changes/ }));
|
||||
|
||||
await waitFor(() => expect(networking.patchAgentCall).toHaveBeenCalledTimes(1));
|
||||
const [token, agentId, payload] = vi.mocked(networking.patchAgentCall).mock.calls[0];
|
||||
expect(token).toBe("sk-test");
|
||||
expect(agentId).toBe("agent-1");
|
||||
expect(payload.tpm_limit).toBe(42);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Card, Title, Text, Button as TremorButton, Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
|
||||
import { Spin, Descriptions } from "antd";
|
||||
import { FormProvider, useForm, useWatch } from "react-hook-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
import { Field, FieldGroup, FieldLabel } from "@/components/shared/form/field";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { ArrowLeftIcon } from "@heroicons/react/outline";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { getAgentInfo, patchAgentCall, getAgentCreateMetadata, AgentCreateInfo } from "@/components/networking";
|
||||
import { Agent } from "@/components/agents/types";
|
||||
import { KeyResponse } from "@/components/key_team_helpers/key_list";
|
||||
|
|
@ -46,6 +47,7 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({ agentId, onClose, accessT
|
|||
const agentKeys = keysData?.keys ?? [];
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const form = useForm<AgentFormValues>({ defaultValues: {} });
|
||||
const panels = useCollapsiblePanels([AGENT_FORM_CONFIG.basic.key]);
|
||||
|
|
@ -203,9 +205,9 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({ agentId, onClose, accessT
|
|||
return (
|
||||
<div className="p-4">
|
||||
<div className="text-center">Agent not found</div>
|
||||
<TremorButton onClick={onClose} className="mt-4">
|
||||
<Button onClick={onClose} className="mt-4">
|
||||
Back to Agents List
|
||||
</TremorButton>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -251,22 +253,29 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({ agentId, onClose, accessT
|
|||
return (
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<TremorButton icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
|
||||
<Button variant="ghost" onClick={onClose} className="mb-4">
|
||||
<ArrowLeft className="size-4" />
|
||||
Back to Agents
|
||||
</TremorButton>
|
||||
<Title>{agent.agent_name || "Unnamed Agent"}</Title>
|
||||
<Text className="text-gray-500 font-mono">{agent.agent_id}</Text>
|
||||
</Button>
|
||||
<h1 className="text-2xl font-semibold">{agent.agent_name || "Unnamed Agent"}</h1>
|
||||
<p className="text-sm text-gray-500 font-mono">{agent.agent_id}</p>
|
||||
</div>
|
||||
|
||||
<TabGroup>
|
||||
<TabList className="mb-4">
|
||||
<Tab key="overview">Overview</Tab>
|
||||
{isAdmin ? <Tab key="settings">Settings</Tab> : <></>}
|
||||
</TabList>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList variant="line" className="mb-4 h-auto w-full justify-start rounded-none border-b p-0">
|
||||
<TabsTrigger value="overview" className="flex-none rounded-none px-4 py-2">
|
||||
Overview
|
||||
</TabsTrigger>
|
||||
{isAdmin && (
|
||||
<TabsTrigger value="settings" className="flex-none rounded-none px-4 py-2">
|
||||
Settings
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
<TabPanels>
|
||||
<div>
|
||||
{/* Overview Panel */}
|
||||
<TabPanel>
|
||||
<TabsContent value="overview" keepMounted>
|
||||
<Descriptions bordered column={1}>
|
||||
<Descriptions.Item label="Agent ID">{agent.agent_id}</Descriptions.Item>
|
||||
<Descriptions.Item label="Agent Name">{agent.agent_name}</Descriptions.Item>
|
||||
|
|
@ -321,7 +330,7 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({ agentId, onClose, accessT
|
|||
(agent.object_permission.mcp_tool_permissions &&
|
||||
Object.keys(agent.object_permission.mcp_tool_permissions).length > 0)) && (
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Title>MCP Tool Permissions</Title>
|
||||
<h3 className="text-lg font-medium">MCP Tool Permissions</h3>
|
||||
<Descriptions bordered column={1} style={{ marginTop: 16 }}>
|
||||
{agent.object_permission.mcp_servers && agent.object_permission.mcp_servers.length > 0 && (
|
||||
<Descriptions.Item label="MCP Servers">
|
||||
|
|
@ -355,7 +364,7 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({ agentId, onClose, accessT
|
|||
|
||||
{agent.agent_card_params?.skills && agent.agent_card_params.skills.length > 0 && (
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Title>Skills</Title>
|
||||
<h3 className="text-lg font-medium">Skills</h3>
|
||||
<Descriptions bordered column={1} style={{ marginTop: 16 }}>
|
||||
{agent.agent_card_params.skills.map((skill: any, index: number) => (
|
||||
<Descriptions.Item label={skill.name || `Skill ${index + 1}`} key={index}>
|
||||
|
|
@ -381,23 +390,23 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({ agentId, onClose, accessT
|
|||
</Descriptions>
|
||||
</div>
|
||||
)}
|
||||
</TabPanel>
|
||||
</TabsContent>
|
||||
|
||||
{/* Settings Panel (only for admins) */}
|
||||
{isAdmin && (
|
||||
<TabPanel>
|
||||
<Card>
|
||||
<TabsContent value="settings" keepMounted>
|
||||
<Card className="block p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Agent Settings</Title>
|
||||
<h3 className="text-lg font-medium">Agent Settings</h3>
|
||||
{!isEditing && (
|
||||
<TremorButton
|
||||
<Button
|
||||
onClick={() => {
|
||||
setAppliedDiscoveredSelection(null);
|
||||
setIsEditing(true);
|
||||
}}
|
||||
>
|
||||
Edit Settings
|
||||
</TremorButton>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -430,7 +439,7 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({ agentId, onClose, accessT
|
|||
)}
|
||||
|
||||
<Separator className="my-6" />
|
||||
<Title className="mb-4">Rate Limits</Title>
|
||||
<h3 className="text-lg font-medium mb-4">Rate Limits</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{rateLimitField("tpm_limit", "TPM Limit")}
|
||||
{rateLimitField("rpm_limit", "RPM Limit")}
|
||||
|
|
@ -452,8 +461,8 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({ agentId, onClose, accessT
|
|||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSaving}>
|
||||
{isSaving && <UiLoadingSpinner />}
|
||||
<Button type="submit" disabled={isSaving} aria-busy={isSaving}>
|
||||
{isSaving && <UiLoadingSpinner className="size-4" />}
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -461,13 +470,13 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({ agentId, onClose, accessT
|
|||
</FormProvider>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<Text>Click "Edit Settings" to modify agent configuration.</Text>
|
||||
<p>Click "Edit Settings" to modify agent configuration.</p>
|
||||
)}
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</TabsContent>
|
||||
)}
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -299,4 +299,30 @@ describe("Guardrail Info", () => {
|
|||
expect(secondCallArgs.litellm_params.patterns).toEqual(["new_pattern"]);
|
||||
expect(secondCallArgs.litellm_params.blocked_words).toEqual(["new_word"]);
|
||||
});
|
||||
|
||||
it("keeps the settings panel mounted while the overview tab is active", async () => {
|
||||
vi.mocked(networking.getGuardrailInfo).mockResolvedValue({
|
||||
guardrail_id: "123",
|
||||
guardrail_name: "Test Guardrail",
|
||||
litellm_params: { guardrail: "presidio", mode: "pre_call", default_on: true },
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
guardrail_definition_location: "database",
|
||||
});
|
||||
vi.mocked(networking.getGuardrailUISettings).mockResolvedValue({
|
||||
supported_entities: [],
|
||||
supported_actions: [],
|
||||
pii_entity_categories: [],
|
||||
supported_modes: ["pre_call"],
|
||||
});
|
||||
vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({});
|
||||
|
||||
const { findByRole, getByRole, getByText } = render(
|
||||
<GuardrailInfoView guardrailId="123" onClose={() => {}} accessToken="123" isAdmin={true} />,
|
||||
);
|
||||
|
||||
expect(await findByRole("tab", { name: "Overview" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(getByRole("tab", { name: "Settings" })).toHaveAttribute("aria-selected", "false");
|
||||
expect(getByText("Guardrail Settings")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@ import {
|
|||
} from "@/components/networking";
|
||||
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
|
||||
import { CodeOutlined, EyeInvisibleOutlined, InfoCircleOutlined, StopOutlined } from "@ant-design/icons";
|
||||
import { ArrowLeftIcon } from "@heroicons/react/outline";
|
||||
import { Badge, Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title } from "@tremor/react";
|
||||
import { Button as AntdButton, Tooltip } from "antd";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { ArrowLeft, CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
|
@ -509,12 +510,12 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
return (
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<AntdButton type="text" icon={<ArrowLeftIcon className="w-4 h-4" />} onClick={onClose} className="mb-4">
|
||||
<AntdButton type="text" icon={<ArrowLeft className="w-4 h-4" />} onClick={onClose} className="mb-4">
|
||||
Back to Guardrails
|
||||
</AntdButton>
|
||||
<Title>{guardrailData.guardrail_name || "Unnamed Guardrail"}</Title>
|
||||
<h1 className="text-2xl font-semibold">{guardrailData.guardrail_name || "Unnamed Guardrail"}</h1>
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<Text className="text-muted-foreground font-mono">{guardrailData.guardrail_id}</Text>
|
||||
<p className="text-muted-foreground font-mono">{guardrailData.guardrail_id}</p>
|
||||
|
||||
<AntdButton
|
||||
type="text"
|
||||
|
|
@ -530,49 +531,55 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<TabGroup>
|
||||
<TabList className="mb-4">
|
||||
<Tab key="overview">Overview</Tab>
|
||||
{isAdmin ? <Tab key="settings">Settings</Tab> : <></>}
|
||||
</TabList>
|
||||
<Tabs defaultValue="overview">
|
||||
<TabsList variant="line" className="mb-4 h-auto w-full justify-start rounded-none border-b p-0">
|
||||
<TabsTrigger value="overview" className="flex-none rounded-none px-4 py-2">
|
||||
Overview
|
||||
</TabsTrigger>
|
||||
{isAdmin && (
|
||||
<TabsTrigger value="settings" className="flex-none rounded-none px-4 py-2">
|
||||
Settings
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
<TabPanels>
|
||||
<div>
|
||||
{/* Overview Panel */}
|
||||
<TabPanel>
|
||||
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
|
||||
<Card>
|
||||
<Text>Provider</Text>
|
||||
<TabsContent value="overview" keepMounted>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<Card className="block p-6">
|
||||
<p>Provider</p>
|
||||
<div className="mt-2 flex items-center space-x-2">
|
||||
<Logo src={logo} label={displayName} className="w-6 h-6" />
|
||||
<Title>{displayName}</Title>
|
||||
<h3 className="text-lg font-medium">{displayName}</h3>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Mode</Text>
|
||||
<Card className="block p-6">
|
||||
<p>Mode</p>
|
||||
<div className="mt-2">
|
||||
<Title>{guardrailData.litellm_params?.mode || "-"}</Title>
|
||||
<Badge color={guardrailData.litellm_params?.default_on ? "green" : "gray"}>
|
||||
<h3 className="text-lg font-medium">{guardrailData.litellm_params?.mode || "-"}</h3>
|
||||
<Badge variant={guardrailData.litellm_params?.default_on ? "secondary" : "outline"}>
|
||||
{guardrailData.litellm_params?.default_on ? "Default On" : "Default Off"}
|
||||
</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Created At</Text>
|
||||
<Card className="block p-6">
|
||||
<p>Created At</p>
|
||||
<div className="mt-2">
|
||||
<Title>{formatDate(guardrailData.created_at)}</Title>
|
||||
<Text>Last Updated: {formatDate(guardrailData.updated_at)}</Text>
|
||||
<h3 className="text-lg font-medium">{formatDate(guardrailData.created_at)}</h3>
|
||||
<p>Last Updated: {formatDate(guardrailData.updated_at)}</p>
|
||||
</div>
|
||||
</Card>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
{guardrailData.litellm_params?.pii_entities_config &&
|
||||
Object.keys(guardrailData.litellm_params.pii_entities_config).length > 0 && (
|
||||
<Card className="mt-6">
|
||||
<Card className="block mt-6 p-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<Text className="font-medium">PII Protection</Text>
|
||||
<Badge color="blue">
|
||||
<p className="font-medium">PII Protection</p>
|
||||
<Badge variant="secondary">
|
||||
{Object.keys(guardrailData.litellm_params.pii_entities_config).length} PII entities configured
|
||||
</Badge>
|
||||
</div>
|
||||
|
|
@ -581,18 +588,18 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
|
||||
{guardrailData.litellm_params?.pii_entities_config &&
|
||||
Object.keys(guardrailData.litellm_params.pii_entities_config).length > 0 && (
|
||||
<Card className="mt-6">
|
||||
<Text className="mb-4 text-lg font-semibold">PII Entity Configuration</Text>
|
||||
<Card className="block mt-6 p-6">
|
||||
<p className="mb-4 text-lg font-semibold">PII Entity Configuration</p>
|
||||
<div className="border rounded-lg overflow-hidden shadow-xs">
|
||||
<div className="bg-muted px-5 py-3 border-b flex">
|
||||
<Text className="flex-1 font-semibold text-foreground">Entity Type</Text>
|
||||
<Text className="flex-1 font-semibold text-foreground">Configuration</Text>
|
||||
<p className="flex-1 font-semibold text-foreground">Entity Type</p>
|
||||
<p className="flex-1 font-semibold text-foreground">Configuration</p>
|
||||
</div>
|
||||
<div className="max-h-[400px] overflow-y-auto">
|
||||
{Object.entries(guardrailData.litellm_params?.pii_entities_config).map(([key, value]) => (
|
||||
<div key={key} className="px-5 py-3 flex border-b hover:bg-muted/50 transition-colors">
|
||||
<Text className="flex-1 font-medium text-foreground">{key}</Text>
|
||||
<Text className="flex-1">
|
||||
<p className="flex-1 font-medium text-foreground">{key}</p>
|
||||
<p className="flex-1">
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 ${
|
||||
value === "MASK" ? "text-blue-600" : "text-red-600"
|
||||
|
|
@ -601,7 +608,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
{value === "MASK" ? <EyeInvisibleOutlined /> : <StopOutlined />}
|
||||
{String(value)}
|
||||
</span>
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -610,18 +617,18 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
)}
|
||||
|
||||
{guardrailData.litellm_params?.guardrail === "tool_permission" && (
|
||||
<Card className="mt-6">
|
||||
<Card className="block mt-6 p-6">
|
||||
<ToolPermissionRulesEditor value={toolPermissionConfig} disabled />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Custom Code Display */}
|
||||
{guardrailData.litellm_params?.guardrail === "custom_code" && guardrailData.litellm_params?.custom_code && (
|
||||
<Card className="mt-6">
|
||||
<Card className="block mt-6 p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<CodeOutlined className="text-blue-500" />
|
||||
<Text className="font-medium text-lg">Custom Code</Text>
|
||||
<p className="font-medium text-lg">Custom Code</p>
|
||||
</div>
|
||||
{isAdmin && !isConfigGuardrail && (
|
||||
<AntdButton size="small" icon={<CodeOutlined />} onClick={() => setCustomCodeModalVisible(true)}>
|
||||
|
|
@ -647,14 +654,14 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
isEditing={false}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabsContent>
|
||||
|
||||
{/* Settings Panel (only for admins) */}
|
||||
{isAdmin && (
|
||||
<TabPanel>
|
||||
<Card>
|
||||
<TabsContent value="settings" keepMounted>
|
||||
<Card className="block p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Guardrail Settings</Title>
|
||||
<h3 className="text-lg font-medium">Guardrail Settings</h3>
|
||||
{isConfigGuardrail && (
|
||||
<Tooltip title="Guardrail is defined in the config file and cannot be edited.">
|
||||
<InfoCircleOutlined />
|
||||
|
|
@ -831,24 +838,24 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
) : (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="font-medium">Guardrail ID</Text>
|
||||
<p className="font-medium">Guardrail ID</p>
|
||||
<div className="font-mono">{guardrailData.guardrail_id}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Guardrail Name</Text>
|
||||
<p className="font-medium">Guardrail Name</p>
|
||||
<div>{guardrailData.guardrail_name || "Unnamed Guardrail"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Provider</Text>
|
||||
<p className="font-medium">Provider</p>
|
||||
<div>{displayName}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Mode</Text>
|
||||
<p className="font-medium">Mode</p>
|
||||
<div>{guardrailData.litellm_params?.mode || "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Default On</Text>
|
||||
<Badge color={guardrailData.litellm_params?.default_on ? "green" : "gray"}>
|
||||
<p className="font-medium">Default On</p>
|
||||
<Badge variant={guardrailData.litellm_params?.default_on ? "secondary" : "outline"}>
|
||||
{guardrailData.litellm_params?.default_on ? "Yes" : "No"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
|
@ -856,9 +863,9 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
{guardrailData.litellm_params?.pii_entities_config &&
|
||||
Object.keys(guardrailData.litellm_params.pii_entities_config).length > 0 && (
|
||||
<div>
|
||||
<Text className="font-medium">PII Protection</Text>
|
||||
<p className="font-medium">PII Protection</p>
|
||||
<div className="mt-2">
|
||||
<Badge color="blue">
|
||||
<Badge variant="secondary">
|
||||
{Object.keys(guardrailData.litellm_params.pii_entities_config).length} PII entities
|
||||
configured
|
||||
</Badge>
|
||||
|
|
@ -867,11 +874,11 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
)}
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Created At</Text>
|
||||
<p className="font-medium">Created At</p>
|
||||
<div>{formatDate(guardrailData.created_at)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Last Updated</Text>
|
||||
<p className="font-medium">Last Updated</p>
|
||||
<div>{formatDate(guardrailData.updated_at)}</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -881,10 +888,10 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</TabsContent>
|
||||
)}
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
</Tabs>
|
||||
|
||||
{/* Custom Code Editor Modal */}
|
||||
<CustomCodeModal
|
||||
|
|
|
|||
|
|
@ -417,12 +417,23 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
|||
Cancel
|
||||
</Button>
|
||||
{scopeType === "specific" && (
|
||||
<Button type="button" variant="secondary" onClick={handlePreviewImpact} disabled={isEstimating}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handlePreviewImpact}
|
||||
disabled={isEstimating}
|
||||
aria-busy={isEstimating}
|
||||
>
|
||||
{isEstimating && <UiLoadingSpinner className="size-4" />}
|
||||
Estimate Impact
|
||||
</Button>
|
||||
)}
|
||||
<Button type="button" onClick={form.handleSubmit(handleSubmit)} disabled={isSubmitting}>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={form.handleSubmit(handleSubmit)}
|
||||
disabled={isSubmitting}
|
||||
aria-busy={isSubmitting}
|
||||
>
|
||||
{isSubmitting && <UiLoadingSpinner className="size-4" />}
|
||||
Create Attachment
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -546,7 +546,12 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
|||
<Button type="button" variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" onClick={form.handleSubmit(handleSubmit)} disabled={isSubmitting}>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={form.handleSubmit(handleSubmit)}
|
||||
disabled={isSubmitting}
|
||||
aria-busy={isSubmitting}
|
||||
>
|
||||
{isSubmitting && <UiLoadingSpinner className="size-4" />}
|
||||
{isEditing ? "Update Policy" : "Create Policy"}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ const PolicyTestPanel: React.FC<PolicyTestPanelProps> = ({ accessToken }) => {
|
|||
</FormField>
|
||||
</FieldGroup>
|
||||
<div className="flex space-x-2 mt-4">
|
||||
<Button type="button" onClick={handleTest} disabled={isLoading || !accessToken}>
|
||||
<Button type="button" onClick={handleTest} disabled={isLoading || !accessToken} aria-busy={isLoading}>
|
||||
{isLoading && <UiLoadingSpinner className="size-4" />}
|
||||
Simulate
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import PromptInfoView from "./prompt_info";
|
||||
import * as networking from "@/components/networking";
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
getPromptInfo: vi.fn(),
|
||||
getPromptVersions: vi.fn(),
|
||||
deletePromptCall: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./prompt_editor_view/PromptCodeSnippets", () => ({
|
||||
default: () => <div data-testid="prompt-code-snippets" />,
|
||||
}));
|
||||
|
||||
const promptWithoutTemplate = {
|
||||
prompt_spec: {
|
||||
prompt_id: "support-reply",
|
||||
version: 1,
|
||||
litellm_params: { prompt_id: "support-reply" },
|
||||
prompt_info: { prompt_type: "dotprompt" },
|
||||
created_by: "admin",
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
},
|
||||
raw_prompt_template: null,
|
||||
environments: [],
|
||||
};
|
||||
|
||||
describe("PromptInfoView tabs", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(networking.getPromptInfo).mockReset().mockResolvedValue(promptWithoutTemplate);
|
||||
vi.mocked(networking.getPromptVersions).mockReset().mockResolvedValue({ prompts: [] });
|
||||
});
|
||||
|
||||
it("shows the raw API response for a prompt that has no template", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<PromptInfoView promptId="support-reply" onClose={vi.fn()} accessToken="sk-test" isAdmin={true} />);
|
||||
|
||||
expect(await screen.findByRole("tab", { name: "Raw JSON" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("tab", { name: "Prompt Template" })).not.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("tab", { name: "Raw JSON" }));
|
||||
|
||||
expect(screen.getByText("Raw API Response")).toBeVisible();
|
||||
expect(screen.getByText(/"prompt_id": "support-reply"/)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,25 +1,10 @@
|
|||
import React, { useState, useEffect, useRef } from "react";
|
||||
import {
|
||||
Card,
|
||||
Title,
|
||||
Text,
|
||||
Grid,
|
||||
Badge,
|
||||
Button as TremorButton,
|
||||
Tab,
|
||||
TabGroup,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "@tremor/react";
|
||||
import { Button, Modal } from "antd";
|
||||
import { ArrowLeftIcon, TrashIcon, PencilIcon } from "@heroicons/react/outline";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Button as AntButton, Modal } from "antd";
|
||||
import {
|
||||
getPromptInfo,
|
||||
getPromptVersions,
|
||||
|
|
@ -28,7 +13,7 @@ import {
|
|||
deletePromptCall,
|
||||
} from "@/components/networking";
|
||||
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { ArrowLeft, CheckIcon, CopyIcon, Pencil, Trash2 } from "lucide-react";
|
||||
import { toast } from "@/lib/toast";
|
||||
import PromptCodeSnippets from "./prompt_editor_view/PromptCodeSnippets";
|
||||
import { extractModel, extractTemplateVariables, getBasePromptId, getCurrentVersion } from "./prompt_utils";
|
||||
|
|
@ -197,15 +182,16 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
return (
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<TremorButton icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
|
||||
<Button variant="ghost" onClick={onClose} className="mb-4">
|
||||
<ArrowLeft className="size-4" />
|
||||
Back to Prompts
|
||||
</TremorButton>
|
||||
</Button>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<Title>Prompt Details</Title>
|
||||
<h1 className="text-2xl font-semibold">Prompt Details</h1>
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<Text className="text-gray-500 font-mono">{basePromptId}</Text>
|
||||
<Button
|
||||
<p className="text-sm text-gray-500 font-mono">{basePromptId}</p>
|
||||
<AntButton
|
||||
type="text"
|
||||
size="small"
|
||||
icon={copiedStates["prompt-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
|
||||
|
|
@ -226,23 +212,15 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
accessToken={accessToken}
|
||||
version={currentVersion}
|
||||
/>
|
||||
<TremorButton
|
||||
icon={PencilIcon}
|
||||
variant="primary"
|
||||
onClick={() => onEdit?.(rawApiResponse)}
|
||||
className="flex items-center"
|
||||
>
|
||||
<Button onClick={() => onEdit?.(rawApiResponse)} className="flex items-center">
|
||||
<Pencil />
|
||||
Prompt Studio
|
||||
</TremorButton>
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
<TremorButton
|
||||
icon={TrashIcon}
|
||||
variant="secondary"
|
||||
onClick={handleDeleteClick}
|
||||
className="flex items-center"
|
||||
>
|
||||
<Button variant="secondary" onClick={handleDeleteClick} className="flex items-center">
|
||||
<Trash2 />
|
||||
Delete Prompt
|
||||
</TremorButton>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -285,81 +263,89 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
{/* Old version banner */}
|
||||
{isViewingOldVersion && (
|
||||
<div className="mb-4 p-3 bg-amber-50 border border-amber-200 rounded-lg flex items-center justify-between">
|
||||
<Text className="text-amber-800">
|
||||
<p className="text-sm text-amber-800">
|
||||
Viewing v{selectedVersion} — not the latest version (v{latestVersion})
|
||||
</Text>
|
||||
<TremorButton
|
||||
variant="light"
|
||||
size="xs"
|
||||
</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const latest = versionHistory.find((v) => v.version === latestVersion);
|
||||
if (latest) handleVersionClick(latest);
|
||||
}}
|
||||
>
|
||||
Go to latest
|
||||
</TremorButton>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabGroup>
|
||||
<TabList className="mb-4">
|
||||
<Tab key="overview">Overview</Tab>
|
||||
{promptTemplate ? <Tab key="prompt-template">Prompt Template</Tab> : <></>}
|
||||
<Tab key="raw-json">Raw JSON</Tab>
|
||||
</TabList>
|
||||
<Tabs defaultValue="overview">
|
||||
<TabsList variant="line" className="mb-4 h-auto w-full justify-start rounded-none border-b p-0">
|
||||
<TabsTrigger value="overview" className="flex-none rounded-none px-4 py-2">
|
||||
Overview
|
||||
</TabsTrigger>
|
||||
{promptTemplate && (
|
||||
<TabsTrigger value="prompt-template" className="flex-none rounded-none px-4 py-2">
|
||||
Prompt Template
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="raw-json" className="flex-none rounded-none px-4 py-2">
|
||||
Raw JSON
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabPanels>
|
||||
<div>
|
||||
{/* Overview Panel */}
|
||||
<TabPanel>
|
||||
<Grid numItems={1} numItemsSm={2} numItemsLg={4} className="gap-4">
|
||||
<Card>
|
||||
<Text>Version</Text>
|
||||
<TabsContent value="overview" keepMounted>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="block p-6">
|
||||
<p>Version</p>
|
||||
<div className="mt-2">
|
||||
<Title>{currentVersion}</Title>
|
||||
<Badge color="blue" className="mt-1">
|
||||
<h3 className="text-lg font-medium">{currentVersion}</h3>
|
||||
<Badge variant="secondary" className="mt-1">
|
||||
v{currentVersion}
|
||||
</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Prompt Type</Text>
|
||||
<Card className="block p-6">
|
||||
<p>Prompt Type</p>
|
||||
<div className="mt-2">
|
||||
<Title>{promptData.prompt_info?.prompt_type || "-"}</Title>
|
||||
<h3 className="text-lg font-medium">{promptData.prompt_info?.prompt_type || "-"}</h3>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Created By</Text>
|
||||
<Card className="block p-6">
|
||||
<p>Created By</p>
|
||||
<div className="mt-2">
|
||||
<Title className="text-sm">{promptData.created_by || "-"}</Title>
|
||||
<h3 className="text-sm font-medium">{promptData.created_by || "-"}</h3>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Created At</Text>
|
||||
<Card className="block p-6">
|
||||
<p>Created At</p>
|
||||
<div className="mt-2">
|
||||
<Title className="text-sm">{formatDate(promptData.created_at)}</Title>
|
||||
<Text className="text-xs">Updated: {formatDate(promptData.updated_at)}</Text>
|
||||
<h3 className="text-sm font-medium">{formatDate(promptData.created_at)}</h3>
|
||||
<p className="text-xs">Updated: {formatDate(promptData.updated_at)}</p>
|
||||
</div>
|
||||
</Card>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
{/* Version History Table */}
|
||||
<Card className="mt-6">
|
||||
<Title className="mb-3">Version History — {selectedEnv}</Title>
|
||||
<Card className="block mt-6 p-6">
|
||||
<h3 className="text-lg font-medium mb-3">Version History — {selectedEnv}</h3>
|
||||
{loadingVersions ? (
|
||||
<Text>Loading versions...</Text>
|
||||
<p>Loading versions...</p>
|
||||
) : versionHistory.length > 0 ? (
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Version</TableHeaderCell>
|
||||
<TableHeaderCell>Created By</TableHeaderCell>
|
||||
<TableHeaderCell>Date</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
<TableHead>Version</TableHead>
|
||||
<TableHead>Created By</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{versionHistory.map((v) => {
|
||||
const vNum = v.version || 1;
|
||||
|
|
@ -376,7 +362,7 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
<TableCell>
|
||||
<span className={isSelected ? "font-bold" : ""}>v{vNum}</span>
|
||||
{isLatest && (
|
||||
<Badge color="blue" className="ml-2" size="xs">
|
||||
<Badge variant="secondary" className="ml-2">
|
||||
latest
|
||||
</Badge>
|
||||
)}
|
||||
|
|
@ -388,10 +374,9 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
<span className="text-sm">{formatDate(v.created_at)}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TremorButton
|
||||
icon={PencilIcon}
|
||||
variant="light"
|
||||
size="xs"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// Build a response-like object for the editor
|
||||
|
|
@ -406,8 +391,9 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
onEdit?.(editData);
|
||||
}}
|
||||
>
|
||||
<Pencil />
|
||||
Edit
|
||||
</TremorButton>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
|
@ -415,18 +401,18 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<Text className="text-gray-400">No versions found in {selectedEnv}</Text>
|
||||
<p className="text-gray-400">No versions found in {selectedEnv}</p>
|
||||
)}
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</TabsContent>
|
||||
|
||||
{/* Prompt Template Panel */}
|
||||
{promptTemplate && (
|
||||
<TabPanel>
|
||||
<Card>
|
||||
<TabsContent value="prompt-template" keepMounted>
|
||||
<Card className="block p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Prompt Template</Title>
|
||||
<Button
|
||||
<h3 className="text-lg font-medium">Prompt Template</h3>
|
||||
<AntButton
|
||||
type="text"
|
||||
size="small"
|
||||
icon={copiedStates["prompt-content"] ? <CheckIcon size={16} /> : <CopyIcon size={16} />}
|
||||
|
|
@ -438,19 +424,19 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
}`}
|
||||
>
|
||||
{copiedStates["prompt-content"] ? "Copied!" : "Copy Content"}
|
||||
</Button>
|
||||
</AntButton>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="font-medium">Template ID</Text>
|
||||
<p className="font-medium">Template ID</p>
|
||||
<div className="font-mono text-sm bg-gray-50 p-2 rounded-sm">
|
||||
{promptTemplate.litellm_prompt_id}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Content</Text>
|
||||
<p className="font-medium">Content</p>
|
||||
<div className="mt-2 p-4 bg-gray-50 rounded-md border overflow-auto max-h-96">
|
||||
<pre className="text-sm text-gray-800 whitespace-pre-wrap">{promptTemplate.content}</pre>
|
||||
</div>
|
||||
|
|
@ -458,7 +444,7 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
|
||||
{promptTemplate.metadata && Object.keys(promptTemplate.metadata).length > 0 && (
|
||||
<div>
|
||||
<Text className="font-medium">Template Metadata</Text>
|
||||
<p className="font-medium">Template Metadata</p>
|
||||
<div className="mt-2 p-3 bg-gray-50 rounded-md border">
|
||||
<pre className="text-xs text-gray-800 whitespace-pre-wrap overflow-auto max-h-64">
|
||||
{JSON.stringify(promptTemplate.metadata, null, 2)}
|
||||
|
|
@ -468,15 +454,15 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{/* Raw JSON Panel */}
|
||||
<TabPanel>
|
||||
<Card>
|
||||
<TabsContent value="raw-json" keepMounted>
|
||||
<Card className="block p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Raw API Response</Title>
|
||||
<Button
|
||||
<h3 className="text-lg font-medium">Raw API Response</h3>
|
||||
<AntButton
|
||||
type="text"
|
||||
size="small"
|
||||
icon={copiedStates["raw-json"] ? <CheckIcon size={16} /> : <CopyIcon size={16} />}
|
||||
|
|
@ -488,7 +474,7 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
}`}
|
||||
>
|
||||
{copiedStates["raw-json"] ? "Copied!" : "Copy JSON"}
|
||||
</Button>
|
||||
</AntButton>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-gray-50 rounded-md border overflow-auto">
|
||||
|
|
@ -497,9 +483,9 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
|
|||
</pre>
|
||||
</div>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
<Modal
|
||||
|
|
|
|||
|
|
@ -375,7 +375,7 @@ const AddPluginForm: React.FC<AddPluginFormProps> = ({ visible, onClose, accessT
|
|||
<Button type="button" variant="outline" onClick={handleCancel} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
<Button type="submit" disabled={isSubmitting} aria-busy={isSubmitting}>
|
||||
{isSubmitting && <UiLoadingSpinner className="size-4" />}
|
||||
{isSubmitting ? "Adding..." : "Add Skill"}
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue