feat(ui): migrate agents subtree to shadcn

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
cursor 2026-04-24 07:57:27 +00:00
parent 550301679c
commit ae8b10cf8d
No known key found for this signature in database
6 changed files with 1923 additions and 967 deletions

View file

@ -52,6 +52,7 @@
"src/components/common_components/AutoRotationView.tsx",
"src/components/agents/agent_card.tsx",
"src/components/agents/agent_table.tsx",
"src/components/agents/add_agent_form.tsx",
"src/components/view_users/columns.tsx",
"src/components/Projects/ProjectsPage.tsx",
"src/components/pass_through_settings.tsx",

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,16 @@
import React from "react";
import { Form, Switch, Collapse, Select } from "antd";
import React, { useState } from "react";
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
@ -9,13 +18,10 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Info, MinusCircle, Plus } from "lucide-react";
import { Info, MinusCircle, Plus, X } from "lucide-react";
import { AGENT_FORM_CONFIG, SKILL_FIELD_CONFIG } from "./agent_config";
import CostConfigFields from "./cost_config_fields";
const { Panel } = Collapse;
const InfoTip: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<TooltipProvider>
<Tooltip>
@ -32,286 +38,573 @@ interface AgentFormFieldsProps {
visiblePanels?: string[];
}
interface FieldProps {
name: string;
label: string;
required?: boolean;
tooltip?: string;
placeholder?: string;
type?: string;
rows?: number;
}
function TextFieldItem({
name,
label,
required,
tooltip,
placeholder,
type,
rows,
}: FieldProps) {
const { register, formState } = useFormContext();
const error = (formState.errors as any)?.[name];
const inputId = `agent-field-${name}`;
return (
<div className="space-y-2">
<Label htmlFor={inputId}>
{label}
{required && <span className="text-destructive"> *</span>}
{tooltip ? <InfoTip>{tooltip}</InfoTip> : null}
</Label>
{type === "textarea" ? (
<Textarea
id={inputId}
rows={rows}
placeholder={placeholder}
aria-invalid={!!error}
{...register(
name,
required
? { required: `Please enter ${label.toLowerCase()}` }
: undefined,
)}
/>
) : (
<Input
id={inputId}
placeholder={placeholder}
aria-invalid={!!error}
{...register(
name,
required
? { required: `Please enter ${label.toLowerCase()}` }
: undefined,
)}
/>
)}
{error && (
<p className="text-sm text-destructive">{error.message as string}</p>
)}
</div>
);
}
function SwitchFieldItem({ name, label }: { name: string; label: string }) {
const { control } = useFormContext();
return (
<div className="flex items-center justify-between">
<Label htmlFor={`agent-switch-${name}`}>{label}</Label>
<Controller
control={control}
name={name}
render={({ field }) => (
<Switch
id={`agent-switch-${name}`}
checked={!!field.value}
onCheckedChange={field.onChange}
/>
)}
/>
</div>
);
}
/**
* Comma-separated tag field. Stored as string[] in the form, rendered as a
* single text input that splits on commas, consistent with the prior antd
* `getValueFromEvent` behavior.
*/
function CommaListField({
name,
label,
required,
placeholder,
}: {
name: string;
label: string;
required?: boolean;
placeholder?: string;
}) {
const { control, formState } = useFormContext();
const error = (formState.errors as any)?.[name.split(".")[0]];
return (
<div className="space-y-2">
<Label htmlFor={`agent-list-${name}`}>
{label}
{required && <span className="text-destructive"> *</span>}
</Label>
<Controller
control={control}
name={name}
rules={
required
? {
validate: (v) =>
Array.isArray(v) && v.length > 0 && v.some((s) => s?.trim())
? true
: "Required",
}
: undefined
}
render={({ field }) => (
<Input
id={`agent-list-${name}`}
placeholder={placeholder}
value={Array.isArray(field.value) ? field.value.join(", ") : ""}
onChange={(e) => {
const raw = e.target.value;
field.onChange(
raw
.split(",")
.map((s) => s.trim())
.filter((s) => s !== "" || required !== true),
);
}}
/>
)}
/>
{error && (
<p className="text-sm text-destructive">
{(error as any).message as string}
</p>
)}
</div>
);
}
function SkillsList() {
const { control } = useFormContext();
const { fields, append, remove } = useFieldArray({
control,
name: "skills",
});
return (
<div className="space-y-4">
{fields.map((f, index) => (
<div
key={f.id}
className="space-y-4 p-4 border border-border rounded-md"
>
<TextFieldItem
name={`skills.${index}.id`}
label={SKILL_FIELD_CONFIG.id.label}
required={SKILL_FIELD_CONFIG.id.required}
placeholder={SKILL_FIELD_CONFIG.id.placeholder}
/>
<TextFieldItem
name={`skills.${index}.name`}
label={SKILL_FIELD_CONFIG.name.label}
required={SKILL_FIELD_CONFIG.name.required}
placeholder={SKILL_FIELD_CONFIG.name.placeholder}
/>
<TextFieldItem
name={`skills.${index}.description`}
label={SKILL_FIELD_CONFIG.description.label}
required={SKILL_FIELD_CONFIG.description.required}
placeholder={SKILL_FIELD_CONFIG.description.placeholder}
type="textarea"
rows={SKILL_FIELD_CONFIG.description.rows}
/>
<CommaListField
name={`skills.${index}.tags`}
label={SKILL_FIELD_CONFIG.tags.label}
required={SKILL_FIELD_CONFIG.tags.required}
placeholder={SKILL_FIELD_CONFIG.tags.placeholder}
/>
<CommaListField
name={`skills.${index}.examples`}
label={SKILL_FIELD_CONFIG.examples.label}
placeholder={SKILL_FIELD_CONFIG.examples.placeholder}
/>
<Button
type="button"
variant="ghost"
className="text-destructive hover:text-destructive"
onClick={() => remove(index)}
>
<MinusCircle className="h-4 w-4" />
Remove Skill
</Button>
</div>
))}
<Button
type="button"
variant="outline"
onClick={() =>
append({ id: "", name: "", description: "", tags: [], examples: [] })
}
className="w-full border-dashed"
>
<Plus className="h-4 w-4" />
Add Skill
</Button>
</div>
);
}
function StaticHeadersList() {
const { control, register } = useFormContext();
const { fields, append, remove } = useFieldArray({
control,
name: "static_headers",
});
return (
<div className="space-y-2">
{fields.map((f, index) => (
<div key={f.id} className="flex items-baseline gap-2 mb-2">
<Input
placeholder="Header name (e.g. Authorization)"
className="w-[220px]"
{...register(`static_headers.${index}.header`, {
required: "Header name required",
})}
/>
<Input
placeholder="Value (e.g. Bearer token123)"
className="w-[260px]"
{...register(`static_headers.${index}.value`, {
required: "Value required",
})}
/>
<button
type="button"
onClick={() => remove(index)}
className="text-destructive"
aria-label="Remove header"
>
<MinusCircle className="h-4 w-4" />
</button>
</div>
))}
<Button
type="button"
variant="outline"
onClick={() => append({ header: "", value: "" })}
className="w-full border-dashed"
>
<Plus className="h-4 w-4" />
Add Static Header
</Button>
</div>
);
}
/**
* Free-form tag input (replacement for `antd.Select mode="tags"`). Stores a
* string[] in form state; users type a value and press Enter/comma to add.
*/
function FreeformTagsField({
name,
placeholder,
}: {
name: string;
placeholder?: string;
}) {
const { control } = useFormContext();
const [draft, setDraft] = useState("");
return (
<Controller
control={control}
name={name}
render={({ field }) => {
const values: string[] = Array.isArray(field.value) ? field.value : [];
const commitDraft = () => {
const parts = draft
.split(",")
.map((s) => s.trim())
.filter((s) => s && !values.includes(s));
if (parts.length > 0) {
field.onChange([...values, ...parts]);
}
setDraft("");
};
return (
<div className="space-y-2">
<Input
placeholder={placeholder}
value={draft}
onChange={(e) => {
const v = e.target.value;
if (v.includes(",")) {
const parts = v
.split(",")
.map((s) => s.trim())
.filter((s) => s && !values.includes(s));
if (parts.length > 0) {
field.onChange([...values, ...parts]);
}
setDraft("");
} else {
setDraft(v);
}
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
commitDraft();
}
}}
onBlur={commitDraft}
/>
{values.length > 0 && (
<div className="flex flex-wrap gap-1">
{values.map((v) => (
<Badge
key={v}
variant="secondary"
className="flex items-center gap-1"
>
{v}
<button
type="button"
onClick={() =>
field.onChange(values.filter((x) => x !== v))
}
className="inline-flex items-center justify-center rounded-full hover:bg-muted-foreground/20"
aria-label={`Remove ${v}`}
>
<X size={12} />
</button>
</Badge>
))}
</div>
)}
</div>
);
}}
/>
);
}
/**
* Reusable form fields component for agent forms
* Uses shared configuration from agent_config.ts
*/
const AgentFormFields: React.FC<AgentFormFieldsProps> = ({ showAgentName = true, visiblePanels }) => {
const shouldShow = (key: string) => !visiblePanels || visiblePanels.includes(key);
const AgentFormFields: React.FC<AgentFormFieldsProps> = ({
showAgentName = true,
visiblePanels,
}) => {
const shouldShow = (key: string) =>
!visiblePanels || visiblePanels.includes(key);
const defaultOpen: string[] = ["basic"];
return (
<>
<div className="space-y-4">
{showAgentName && (
<Form.Item
label="Agent Name"
<TextFieldItem
name="agent_name"
rules={[{ required: true, message: "Please enter a unique agent name" }]}
label="Agent Name"
required
tooltip="Unique identifier for the agent"
>
<Input placeholder="e.g., customer-support-agent" />
</Form.Item>
placeholder="e.g., customer-support-agent"
/>
)}
<Collapse defaultActiveKey={['basic']} style={{ marginBottom: 16 }}>
<Accordion
type="multiple"
defaultValue={defaultOpen}
className="mb-4 space-y-2"
>
{/* Basic Information */}
{shouldShow(AGENT_FORM_CONFIG.basic.key) && (
<Panel header={`${AGENT_FORM_CONFIG.basic.title} (Required)`} key={AGENT_FORM_CONFIG.basic.key}>
{AGENT_FORM_CONFIG.basic.fields.map((field) => (
<Form.Item
key={field.name}
label={field.label}
name={field.name}
rules={field.required ? [{ required: true, message: `Please enter ${field.label.toLowerCase()}` }] : undefined}
tooltip={field.tooltip}
>
{field.type === 'textarea' ? (
<Textarea rows={field.rows} placeholder={field.placeholder} />
) : (
<Input placeholder={field.placeholder} />
)}
</Form.Item>
))}
</Panel>
<AccordionItem
value={AGENT_FORM_CONFIG.basic.key}
className="border border-border rounded-md px-3"
>
<AccordionTrigger className="py-2 hover:no-underline">
{AGENT_FORM_CONFIG.basic.title} (Required)
</AccordionTrigger>
<AccordionContent>
<div className="space-y-4">
{AGENT_FORM_CONFIG.basic.fields.map((field) => (
<TextFieldItem
key={field.name}
name={field.name}
label={field.label}
required={field.required}
tooltip={field.tooltip}
placeholder={field.placeholder}
type={field.type === "textarea" ? "textarea" : undefined}
rows={field.rows}
/>
))}
</div>
</AccordionContent>
</AccordionItem>
)}
{/* Skills */}
{shouldShow(AGENT_FORM_CONFIG.skills.key) && (
<Panel header={`${AGENT_FORM_CONFIG.skills.title} (Required)`} key={AGENT_FORM_CONFIG.skills.key}>
<Form.List name="skills">
{(fields, { add, remove }) => (
<>
{fields.map((field) => (
<div key={field.key} style={{ marginBottom: 16, padding: 16, border: '1px solid #d9d9d9', borderRadius: 4 }}>
<Form.Item
{...field}
label={SKILL_FIELD_CONFIG.id.label}
name={[field.name, 'id']}
rules={[{ required: SKILL_FIELD_CONFIG.id.required, message: 'Required' }]}
>
<Input placeholder={SKILL_FIELD_CONFIG.id.placeholder} />
</Form.Item>
<Form.Item
{...field}
label={SKILL_FIELD_CONFIG.name.label}
name={[field.name, 'name']}
rules={[{ required: SKILL_FIELD_CONFIG.name.required, message: 'Required' }]}
>
<Input placeholder={SKILL_FIELD_CONFIG.name.placeholder} />
</Form.Item>
<Form.Item
{...field}
label={SKILL_FIELD_CONFIG.description.label}
name={[field.name, 'description']}
rules={[{ required: SKILL_FIELD_CONFIG.description.required, message: 'Required' }]}
>
<Textarea rows={SKILL_FIELD_CONFIG.description.rows} placeholder={SKILL_FIELD_CONFIG.description.placeholder} />
</Form.Item>
<Form.Item
{...field}
label={SKILL_FIELD_CONFIG.tags.label}
name={[field.name, 'tags']}
rules={[{ required: SKILL_FIELD_CONFIG.tags.required, message: 'Required' }]}
getValueFromEvent={(e) => e.target.value.split(',').map((s: string) => s.trim())}
getValueProps={(value) => ({ value: Array.isArray(value) ? value.join(', ') : value })}
>
<Input placeholder={SKILL_FIELD_CONFIG.tags.placeholder} />
</Form.Item>
<Form.Item
{...field}
label={SKILL_FIELD_CONFIG.examples.label}
name={[field.name, 'examples']}
getValueFromEvent={(e) => e.target.value.split(',').map((s: string) => s.trim()).filter((s: string) => s)}
getValueProps={(value) => ({ value: Array.isArray(value) ? value.join(', ') : '' })}
>
<Input placeholder={SKILL_FIELD_CONFIG.examples.placeholder} />
</Form.Item>
<Button
type="button"
variant="ghost"
className="text-destructive hover:text-destructive"
onClick={() => remove(field.name)}
>
<MinusCircle className="h-4 w-4" />
Remove Skill
</Button>
</div>
))}
<Button
type="button"
variant="outline"
onClick={() => add()}
className="w-full border-dashed"
>
<Plus className="h-4 w-4" />
Add Skill
</Button>
</>
)}
</Form.List>
</Panel>
<AccordionItem
value={AGENT_FORM_CONFIG.skills.key}
className="border border-border rounded-md px-3"
>
<AccordionTrigger className="py-2 hover:no-underline">
{AGENT_FORM_CONFIG.skills.title} (Required)
</AccordionTrigger>
<AccordionContent>
<SkillsList />
</AccordionContent>
</AccordionItem>
)}
{/* Capabilities */}
{shouldShow(AGENT_FORM_CONFIG.capabilities.key) && (
<Panel header={AGENT_FORM_CONFIG.capabilities.title} key={AGENT_FORM_CONFIG.capabilities.key}>
{AGENT_FORM_CONFIG.capabilities.fields.map((field) => (
<Form.Item
key={field.name}
label={field.label}
name={field.name}
valuePropName="checked"
>
<Switch />
</Form.Item>
))}
</Panel>
<AccordionItem
value={AGENT_FORM_CONFIG.capabilities.key}
className="border border-border rounded-md px-3"
>
<AccordionTrigger className="py-2 hover:no-underline">
{AGENT_FORM_CONFIG.capabilities.title}
</AccordionTrigger>
<AccordionContent>
<div className="space-y-4">
{AGENT_FORM_CONFIG.capabilities.fields.map((field) => (
<SwitchFieldItem
key={field.name}
name={field.name}
label={field.label}
/>
))}
</div>
</AccordionContent>
</AccordionItem>
)}
{/* Optional Settings */}
{shouldShow(AGENT_FORM_CONFIG.optional.key) && (
<Panel header={AGENT_FORM_CONFIG.optional.title} key={AGENT_FORM_CONFIG.optional.key}>
{AGENT_FORM_CONFIG.optional.fields.map((field) => (
<Form.Item
key={field.name}
label={field.label}
name={field.name}
valuePropName={field.type === 'switch' ? 'checked' : undefined}
>
{field.type === 'switch' ? <Switch /> : <Input placeholder={field.placeholder} />}
</Form.Item>
))}
</Panel>
<AccordionItem
value={AGENT_FORM_CONFIG.optional.key}
className="border border-border rounded-md px-3"
>
<AccordionTrigger className="py-2 hover:no-underline">
{AGENT_FORM_CONFIG.optional.title}
</AccordionTrigger>
<AccordionContent>
<div className="space-y-4">
{AGENT_FORM_CONFIG.optional.fields.map((field) =>
field.type === "switch" ? (
<SwitchFieldItem
key={field.name}
name={field.name}
label={field.label}
/>
) : (
<TextFieldItem
key={field.name}
name={field.name}
label={field.label}
placeholder={field.placeholder}
/>
),
)}
</div>
</AccordionContent>
</AccordionItem>
)}
{/* Cost Configuration */}
{shouldShow(AGENT_FORM_CONFIG.cost.key) && (
<Panel header={AGENT_FORM_CONFIG.cost.title} key={AGENT_FORM_CONFIG.cost.key}>
<CostConfigFields />
</Panel>
<AccordionItem
value={AGENT_FORM_CONFIG.cost.key}
className="border border-border rounded-md px-3"
>
<AccordionTrigger className="py-2 hover:no-underline">
{AGENT_FORM_CONFIG.cost.title}
</AccordionTrigger>
<AccordionContent>
<CostConfigFields />
</AccordionContent>
</AccordionItem>
)}
{/* LiteLLM Parameters */}
{shouldShow(AGENT_FORM_CONFIG.litellm.key) && (
<Panel header={AGENT_FORM_CONFIG.litellm.title} key={AGENT_FORM_CONFIG.litellm.key}>
{AGENT_FORM_CONFIG.litellm.fields.map((field) => (
<Form.Item
key={field.name}
label={field.label}
name={field.name}
valuePropName={field.type === 'switch' ? 'checked' : undefined}
>
{field.type === 'switch' ? <Switch /> : <Input placeholder={field.placeholder} />}
</Form.Item>
))}
</Panel>
<AccordionItem
value={AGENT_FORM_CONFIG.litellm.key}
className="border border-border rounded-md px-3"
>
<AccordionTrigger className="py-2 hover:no-underline">
{AGENT_FORM_CONFIG.litellm.title}
</AccordionTrigger>
<AccordionContent>
<div className="space-y-4">
{AGENT_FORM_CONFIG.litellm.fields.map((field) =>
field.type === "switch" ? (
<SwitchFieldItem
key={field.name}
name={field.name}
label={field.label}
/>
) : (
<TextFieldItem
key={field.name}
name={field.name}
label={field.label}
placeholder={field.placeholder}
/>
),
)}
</div>
</AccordionContent>
</AccordionItem>
)}
{/* Authentication Headers */}
{shouldShow("auth_headers") && (
<Panel header="Authentication Headers" key="auth_headers">
{/* Static Headers */}
<Form.Item
label={
<span>
Static Headers{" "}
<InfoTip>
Headers always sent to the backend agent, regardless of the
client request. Admin-configured, static wins on conflict.
</InfoTip>
</span>
}
<AccordionItem
value="auth_headers"
className="border border-border rounded-md px-3"
>
<Form.List name="static_headers">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<div
key={key}
className="flex items-baseline gap-2 mb-2"
>
<Form.Item
{...restField}
name={[name, "header"]}
rules={[
{
required: true,
message: "Header name required",
},
]}
className="mb-0"
>
<Input
placeholder="Header name (e.g. Authorization)"
className="w-[220px]"
/>
</Form.Item>
<Form.Item
{...restField}
name={[name, "value"]}
rules={[
{ required: true, message: "Value required" },
]}
className="mb-0"
>
<Input
placeholder="Value (e.g. Bearer token123)"
className="w-[260px]"
/>
</Form.Item>
<button
type="button"
onClick={() => remove(name)}
className="text-destructive"
aria-label="Remove header"
>
<MinusCircle className="h-4 w-4" />
</button>
</div>
))}
<Button
type="button"
variant="outline"
onClick={() => add()}
className="w-full border-dashed"
>
<Plus className="h-4 w-4" />
Add Static Header
</Button>
</>
)}
</Form.List>
</Form.Item>
<AccordionTrigger className="py-2 hover:no-underline">
Authentication Headers
</AccordionTrigger>
<AccordionContent>
<div className="space-y-4">
<div className="space-y-2">
<Label>
Static Headers
<InfoTip>
Headers always sent to the backend agent, regardless of
the client request. Admin-configured, static wins on
conflict.
</InfoTip>
</Label>
<StaticHeadersList />
</div>
{/* Extra Headers (dynamic forwarding) */}
<Form.Item
label={
<span>
Forward Client Headers{" "}
<InfoTip>
Header names to extract from the client&apos;s request and
forward to the agent. Type a name and press Enter.
</InfoTip>
</span>
}
name="extra_headers"
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="e.g. x-api-key, Authorization"
tokenSeparators={[","]}
/>
</Form.Item>
</Panel>
<div className="space-y-2">
<Label htmlFor="agent-extra-headers">
Forward Client Headers
<InfoTip>
Header names to extract from the client&apos;s request
and forward to the agent. Type a name and press Enter.
</InfoTip>
</Label>
<FreeformTagsField
name="extra_headers"
placeholder="e.g. x-api-key, Authorization"
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
)}
</Collapse>
</>
</Accordion>
</div>
);
};
export default AgentFormFields;

View file

@ -1,9 +1,10 @@
import React, { useState, useEffect, useCallback } from "react";
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
import { Form, Input, InputNumber, Descriptions } from "antd";
import { FormProvider, useForm } from "react-hook-form";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ArrowLeft, Loader2 } from "lucide-react";
import MessageManager from "@/components/molecules/message_manager";
import { getAgentInfo, patchAgentCall, getAgentCreateMetadata, AgentCreateInfo } from "../networking";
@ -21,6 +22,31 @@ interface AgentInfoViewProps {
isAdmin: boolean;
}
function DescRow({
label,
children,
}: {
label: React.ReactNode;
children: React.ReactNode;
}) {
return (
<div className="grid grid-cols-[220px_1fr] border-t border-border first:border-t-0">
<dt className="bg-muted px-4 py-3 text-sm font-medium text-foreground border-r border-border">
{label}
</dt>
<dd className="px-4 py-3 text-sm text-foreground">{children}</dd>
</div>
);
}
function DescList({ children }: { children: React.ReactNode }) {
return (
<dl className="border border-border rounded-md overflow-hidden">
{children}
</dl>
);
}
const AgentInfoView: React.FC<AgentInfoViewProps> = ({
agentId,
onClose,
@ -31,7 +57,8 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({
const [isLoading, setIsLoading] = useState(true);
const [isEditing, setIsEditing] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [form] = Form.useForm();
const form = useForm<any>({ defaultValues: {} });
const { register, reset, handleSubmit } = form;
const [agentTypeMetadata, setAgentTypeMetadata] = useState<AgentCreateInfo[]>([]);
const [detectedAgentType, setDetectedAgentType] = useState<string>("a2a");
@ -59,15 +86,15 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({
setDetectedAgentType(agentType);
if (agentType === "a2a") {
form.setFieldsValue(parseAgentForForm(data));
reset(parseAgentForForm(data));
} else {
const typeInfo = agentTypeMetadata.find(
(t) => t.agent_type === agentType,
);
if (typeInfo) {
form.setFieldsValue(parseDynamicAgentForForm(data, typeInfo));
reset(parseDynamicAgentForForm(data, typeInfo));
} else {
form.setFieldsValue(parseAgentForForm(data));
reset(parseAgentForForm(data));
}
}
} catch (error) {
@ -76,13 +103,12 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({
} finally {
setIsLoading(false);
}
}, [accessToken, agentId, agentTypeMetadata, form]);
}, [accessToken, agentId, agentTypeMetadata, reset]);
useEffect(() => {
fetchAgentInfo();
}, [fetchAgentInfo]);
// Re-parse form when metadata is loaded
useEffect(() => {
if (agent && agentTypeMetadata.length > 0) {
const agentType = detectAgentType(agent);
@ -91,36 +117,32 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({
(t) => t.agent_type === agentType,
);
if (typeInfo) {
form.setFieldsValue(parseDynamicAgentForForm(agent, typeInfo));
reset(parseDynamicAgentForForm(agent, typeInfo));
}
}
}
}, [agentTypeMetadata, agent, form]);
}, [agentTypeMetadata, agent, reset]);
const selectedAgentTypeInfo = agentTypeMetadata.find(
(t) => t.agent_type === detectedAgentType,
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleUpdate = async (values: any) => {
if (!accessToken || !agent) return;
setIsSaving(true);
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let updateData: any;
if (detectedAgentType === "a2a") {
updateData = buildAgentDataFromForm(values, agent);
} else if (selectedAgentTypeInfo) {
updateData = buildDynamicAgentData(values, selectedAgentTypeInfo);
// Preserve the agent_name from form
updateData.agent_name = values.agent_name;
} else {
updateData = buildAgentDataFromForm(values, agent);
}
await patchAgentCall(accessToken, agentId, updateData);
MessageManager.success("Agent updated successfully");
setIsEditing(false);
@ -154,7 +176,6 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({
);
}
// Format date helper function
const formatDate = (dateString?: string) => {
if (!dateString) return "-";
const date = new Date(dateString);
@ -174,104 +195,135 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({
<span className="text-muted-foreground font-mono">{agent.agent_id}</span>
</div>
<TabGroup>
<TabList className="mb-4">
<Tab key="overview">Overview</Tab>
{isAdmin ? <Tab key="settings">Settings</Tab> : <></>}
</TabList>
<Tabs defaultValue="overview" className="mt-4">
<TabsList className="mb-4">
<TabsTrigger value="overview">Overview</TabsTrigger>
{isAdmin && <TabsTrigger value="settings">Settings</TabsTrigger>}
</TabsList>
<TabPanels>
{/* Overview Panel */}
<TabPanel>
<Descriptions bordered column={1}>
<Descriptions.Item label="Agent ID">{agent.agent_id}</Descriptions.Item>
<Descriptions.Item label="Agent Name">{agent.agent_name}</Descriptions.Item>
<Descriptions.Item label="Display Name">{agent.agent_card_params?.name || "-"}</Descriptions.Item>
<Descriptions.Item label="Description">{agent.agent_card_params?.description || "-"}</Descriptions.Item>
<Descriptions.Item label="URL">{agent.agent_card_params?.url || "-"}</Descriptions.Item>
<Descriptions.Item label="Version">{agent.agent_card_params?.version || "-"}</Descriptions.Item>
<Descriptions.Item label="Protocol Version">{agent.agent_card_params?.protocolVersion || "-"}</Descriptions.Item>
<Descriptions.Item label="Streaming">
{agent.agent_card_params?.capabilities?.streaming ? "Yes" : "No"}
</Descriptions.Item>
{agent.agent_card_params?.capabilities?.pushNotifications && (
<Descriptions.Item label="Push Notifications">Yes</Descriptions.Item>
)}
{agent.agent_card_params?.capabilities?.stateTransitionHistory && (
<Descriptions.Item label="State Transition History">Yes</Descriptions.Item>
)}
<Descriptions.Item label="Skills">
{agent.agent_card_params?.skills?.length || 0} configured
</Descriptions.Item>
{agent.litellm_params?.model && (
<Descriptions.Item label="Model">{agent.litellm_params.model}</Descriptions.Item>
)}
{agent.litellm_params?.make_public !== undefined && (
<Descriptions.Item label="Make Public">{agent.litellm_params.make_public ? "Yes" : "No"}</Descriptions.Item>
)}
{agent.agent_card_params?.iconUrl && (
<Descriptions.Item label="Icon URL">{agent.agent_card_params.iconUrl}</Descriptions.Item>
)}
{agent.agent_card_params?.documentationUrl && (
<Descriptions.Item label="Documentation URL">{agent.agent_card_params.documentationUrl}</Descriptions.Item>
)}
<Descriptions.Item label="TPM Limit">{agent.tpm_limit ?? "Unlimited"}</Descriptions.Item>
<Descriptions.Item label="RPM Limit">{agent.rpm_limit ?? "Unlimited"}</Descriptions.Item>
<Descriptions.Item label="Session TPM Limit">{agent.session_tpm_limit ?? "Unlimited"}</Descriptions.Item>
<Descriptions.Item label="Session RPM Limit">{agent.session_rpm_limit ?? "Unlimited"}</Descriptions.Item>
<Descriptions.Item label="Created At">{formatDate(agent.created_at)}</Descriptions.Item>
<Descriptions.Item label="Updated At">{formatDate(agent.updated_at)}</Descriptions.Item>
</Descriptions>
<TabsContent value="overview">
<DescList>
<DescRow label="Agent ID">{agent.agent_id}</DescRow>
<DescRow label="Agent Name">{agent.agent_name}</DescRow>
<DescRow label="Display Name">
{agent.agent_card_params?.name || "-"}
</DescRow>
<DescRow label="Description">
{agent.agent_card_params?.description || "-"}
</DescRow>
<DescRow label="URL">
{agent.agent_card_params?.url || "-"}
</DescRow>
<DescRow label="Version">
{agent.agent_card_params?.version || "-"}
</DescRow>
<DescRow label="Protocol Version">
{agent.agent_card_params?.protocolVersion || "-"}
</DescRow>
<DescRow label="Streaming">
{agent.agent_card_params?.capabilities?.streaming ? "Yes" : "No"}
</DescRow>
{agent.agent_card_params?.capabilities?.pushNotifications && (
<DescRow label="Push Notifications">Yes</DescRow>
)}
{agent.agent_card_params?.capabilities?.stateTransitionHistory && (
<DescRow label="State Transition History">Yes</DescRow>
)}
<DescRow label="Skills">
{agent.agent_card_params?.skills?.length || 0} configured
</DescRow>
{agent.litellm_params?.model && (
<DescRow label="Model">{agent.litellm_params.model}</DescRow>
)}
{agent.litellm_params?.make_public !== undefined && (
<DescRow label="Make Public">
{agent.litellm_params.make_public ? "Yes" : "No"}
</DescRow>
)}
{agent.agent_card_params?.iconUrl && (
<DescRow label="Icon URL">
{agent.agent_card_params.iconUrl}
</DescRow>
)}
{agent.agent_card_params?.documentationUrl && (
<DescRow label="Documentation URL">
{agent.agent_card_params.documentationUrl}
</DescRow>
)}
<DescRow label="TPM Limit">
{agent.tpm_limit ?? "Unlimited"}
</DescRow>
<DescRow label="RPM Limit">
{agent.rpm_limit ?? "Unlimited"}
</DescRow>
<DescRow label="Session TPM Limit">
{agent.session_tpm_limit ?? "Unlimited"}
</DescRow>
<DescRow label="Session RPM Limit">
{agent.session_rpm_limit ?? "Unlimited"}
</DescRow>
<DescRow label="Created At">{formatDate(agent.created_at)}</DescRow>
<DescRow label="Updated At">{formatDate(agent.updated_at)}</DescRow>
</DescList>
{agent.object_permission &&
(agent.object_permission.mcp_servers?.length ||
agent.object_permission.mcp_access_groups?.length ||
(agent.object_permission.mcp_tool_permissions &&
Object.keys(agent.object_permission.mcp_tool_permissions).length > 0)) && (
{agent.object_permission &&
(agent.object_permission.mcp_servers?.length ||
agent.object_permission.mcp_access_groups?.length ||
(agent.object_permission.mcp_tool_permissions &&
Object.keys(agent.object_permission.mcp_tool_permissions)
.length > 0)) && (
<div className="mt-6">
<h2 className="text-lg font-semibold">MCP Tool Permissions</h2>
<Descriptions bordered column={1} className="mt-4">
{agent.object_permission.mcp_servers && agent.object_permission.mcp_servers.length > 0 && (
<Descriptions.Item label="MCP Servers">
{agent.object_permission.mcp_servers.join(", ")}
</Descriptions.Item>
)}
{agent.object_permission.mcp_access_groups &&
agent.object_permission.mcp_access_groups.length > 0 && (
<Descriptions.Item label="MCP Access Groups">
{agent.object_permission.mcp_access_groups.join(", ")}
</Descriptions.Item>
)}
{agent.object_permission.mcp_tool_permissions &&
Object.keys(agent.object_permission.mcp_tool_permissions).length > 0 && (
<Descriptions.Item label="Tool permissions per server">
<div className="space-y-1">
{Object.entries(agent.object_permission.mcp_tool_permissions).map(
([serverId, tools]) => (
<h2 className="text-lg font-semibold">
MCP Tool Permissions
</h2>
<div className="mt-4">
<DescList>
{agent.object_permission.mcp_servers &&
agent.object_permission.mcp_servers.length > 0 && (
<DescRow label="MCP Servers">
{agent.object_permission.mcp_servers.join(", ")}
</DescRow>
)}
{agent.object_permission.mcp_access_groups &&
agent.object_permission.mcp_access_groups.length > 0 && (
<DescRow label="MCP Access Groups">
{agent.object_permission.mcp_access_groups.join(", ")}
</DescRow>
)}
{agent.object_permission.mcp_tool_permissions &&
Object.keys(agent.object_permission.mcp_tool_permissions)
.length > 0 && (
<DescRow label="Tool permissions per server">
<div className="space-y-1">
{Object.entries(
agent.object_permission.mcp_tool_permissions,
).map(([serverId, tools]) => (
<div key={serverId}>
<span className="font-medium">{serverId}:</span>{" "}
{Array.isArray(tools) ? tools.join(", ") : String(tools)}
{Array.isArray(tools)
? tools.join(", ")
: String(tools)}
</div>
)
)}
</div>
</Descriptions.Item>
)}
</Descriptions>
))}
</div>
</DescRow>
)}
</DescList>
</div>
</div>
)}
<AgentCostView agent={agent} />
<AgentCostView agent={agent} />
{agent.agent_card_params?.skills &&
agent.agent_card_params.skills.length > 0 && (
<div className="mt-6">
<h2 className="text-lg font-semibold">Skills</h2>
<Descriptions bordered column={1} className="mt-4">
{agent.agent_card_params?.skills &&
agent.agent_card_params.skills.length > 0 && (
<div className="mt-6">
<h2 className="text-lg font-semibold">Skills</h2>
<div className="mt-4">
<DescList>
{agent.agent_card_params.skills.map(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(skill: any, index: number) => (
<Descriptions.Item
<DescRow
label={skill.name || `Skill ${index + 1}`}
key={index}
>
@ -297,62 +349,106 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({
</div>
)}
</div>
</Descriptions.Item>
</DescRow>
),
)}
</Descriptions>
</DescList>
</div>
)}
</TabPanel>
</div>
)}
</TabsContent>
{/* Settings Panel (only for admins) */}
{isAdmin && (
<TabPanel>
<Card className="p-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold">Agent Settings</h2>
{!isEditing && (
<Button onClick={() => setIsEditing(true)}>
Edit Settings
</Button>
)}
</div>
{isAdmin && (
<TabsContent value="settings">
<Card className="p-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold">Agent Settings</h2>
{!isEditing && (
<Button onClick={() => setIsEditing(true)}>
Edit Settings
</Button>
)}
</div>
{isEditing ? (
<Form
form={form}
layout="vertical"
onFinish={handleUpdate}
{isEditing ? (
<FormProvider {...form}>
<form
onSubmit={handleSubmit(handleUpdate)}
className="space-y-4"
>
<Form.Item label="Agent ID">
<Input value={agent.agent_id} disabled />
</Form.Item>
<div className="space-y-2">
<Label htmlFor="agent-id-readonly">Agent ID</Label>
<Input
id="agent-id-readonly"
value={agent.agent_id}
disabled
readOnly
/>
</div>
{detectedAgentType === "a2a" ? (
<AgentFormFields showAgentName={true} />
) : selectedAgentTypeInfo ? (
<DynamicAgentFormFields agentTypeInfo={selectedAgentTypeInfo} />
<DynamicAgentFormFields
agentTypeInfo={selectedAgentTypeInfo}
/>
) : (
<AgentFormFields showAgentName={true} />
<AgentFormFields showAgentName={true} />
)}
<hr className="my-4 border-border" />
<h3 className="text-lg font-semibold mb-4">Rate Limits</h3>
<div className="grid grid-cols-2 gap-4">
<Form.Item label="TPM Limit" name="tpm_limit">
<InputNumber className="w-full" min={0} placeholder="Unlimited" />
</Form.Item>
<Form.Item label="RPM Limit" name="rpm_limit">
<InputNumber className="w-full" min={0} placeholder="Unlimited" />
</Form.Item>
<div className="space-y-2">
<Label htmlFor="tpm_limit">TPM Limit</Label>
<Input
id="tpm_limit"
type="number"
min={0}
placeholder="Unlimited"
{...register("tpm_limit", { valueAsNumber: true })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="rpm_limit">RPM Limit</Label>
<Input
id="rpm_limit"
type="number"
min={0}
placeholder="Unlimited"
{...register("rpm_limit", { valueAsNumber: true })}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<Form.Item label="Session TPM Limit" name="session_tpm_limit">
<InputNumber className="w-full" min={0} placeholder="Unlimited" />
</Form.Item>
<Form.Item label="Session RPM Limit" name="session_rpm_limit">
<InputNumber className="w-full" min={0} placeholder="Unlimited" />
</Form.Item>
<div className="space-y-2">
<Label htmlFor="session_tpm_limit">
Session TPM Limit
</Label>
<Input
id="session_tpm_limit"
type="number"
min={0}
placeholder="Unlimited"
{...register("session_tpm_limit", {
valueAsNumber: true,
})}
/>
</div>
<div className="space-y-2">
<Label htmlFor="session_rpm_limit">
Session RPM Limit
</Label>
<Input
id="session_rpm_limit"
type="number"
min={0}
placeholder="Unlimited"
{...register("session_rpm_limit", {
valueAsNumber: true,
})}
/>
</div>
</div>
<div className="flex justify-end gap-2 mt-6">
@ -370,21 +466,19 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({
{isSaving ? "Saving..." : "Save Changes"}
</Button>
</div>
</Form>
) : (
<p className="text-muted-foreground">
Click &quot;Edit Settings&quot; to modify agent
configuration.
</p>
)}
</Card>
</TabPanel>
)}
</TabPanels>
</TabGroup>
</form>
</FormProvider>
) : (
<p className="text-muted-foreground">
Click &quot;Edit Settings&quot; to modify agent configuration.
</p>
)}
</Card>
</TabsContent>
)}
</Tabs>
</div>
);
};
export default AgentInfoView;

View file

@ -1,26 +1,47 @@
import React from "react";
import { Form } from "antd";
import { useFormContext } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Info } from "lucide-react";
import { AGENT_FORM_CONFIG } from "./agent_config";
const InfoTip: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="ml-1 inline h-3 w-3 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">{children}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
const CostConfigFields: React.FC = () => {
const { register } = useFormContext();
return (
<>
<div className="space-y-4">
{AGENT_FORM_CONFIG.cost.fields.map((field) => (
<Form.Item
key={field.name}
label={field.label}
name={field.name}
tooltip={field.tooltip}
>
<div key={field.name} className="space-y-2">
<Label htmlFor={field.name}>
{field.label}
{field.tooltip ? <InfoTip>{field.tooltip}</InfoTip> : null}
</Label>
<Input
id={field.name}
placeholder={field.placeholder}
type="number"
step="0.000001"
{...register(field.name)}
/>
</Form.Item>
</div>
))}
</>
</div>
);
};

View file

@ -1,5 +1,5 @@
import React from "react";
import { Form, Input as AntInput, Select } from "antd";
import React, { useEffect } from "react";
import { Controller, useFormContext } from "react-hook-form";
import {
Accordion,
AccordionContent,
@ -7,7 +7,22 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Info } from "lucide-react";
import { AgentCreateInfo, AgentCredentialFieldMetadata } from "../networking";
import { AGENT_FORM_CONFIG } from "./agent_config";
import CostConfigFields from "./cost_config_fields";
@ -16,6 +31,17 @@ interface DynamicAgentFormFieldsProps {
agentTypeInfo: AgentCreateInfo;
}
const InfoTip: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="ml-1 inline h-3 w-3 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">{children}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
/**
* Form fields for dynamic agent types (e.g., LangGraph).
* Renders common fields (agent name, display name, description) plus
@ -24,66 +50,146 @@ interface DynamicAgentFormFieldsProps {
const DynamicAgentFormFields: React.FC<DynamicAgentFormFieldsProps> = ({
agentTypeInfo,
}) => {
return (
<>
<Form.Item
label="Agent Name"
name="agent_name"
rules={[
{ required: true, message: "Please enter a unique agent name" },
]}
tooltip="Unique identifier for the agent"
>
<Input placeholder="e.g., my-langgraph-agent" />
</Form.Item>
const { register, control, setValue, formState, getValues } =
useFormContext();
<Form.Item
label="Description"
name="description"
tooltip="Brief description of what this agent does"
>
useEffect(() => {
for (const field of agentTypeInfo.credential_fields) {
if (field.default_value !== undefined) {
const current = getValues(field.key);
if (current === undefined || current === null || current === "") {
setValue(field.key, field.default_value);
}
}
}
}, [agentTypeInfo, setValue, getValues]);
const agentNameError = (formState.errors as any)?.agent_name;
return (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="agent_name">
Agent Name <span className="text-destructive">*</span>
<InfoTip>Unique identifier for the agent</InfoTip>
</Label>
<Input
id="agent_name"
placeholder="e.g., my-langgraph-agent"
aria-invalid={!!agentNameError}
{...register("agent_name", {
required: "Please enter a unique agent name",
})}
/>
{agentNameError && (
<p className="text-sm text-destructive">
{agentNameError.message as string}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="description">
Description
<InfoTip>Brief description of what this agent does</InfoTip>
</Label>
<Textarea
id="description"
rows={2}
placeholder="Describe what this agent does..."
{...register("description")}
/>
</Form.Item>
</div>
{agentTypeInfo.credential_fields.map(
(field: AgentCredentialFieldMetadata) => (
<Form.Item
key={field.key}
label={field.label}
name={field.key}
rules={
field.required
? [
{
required: true,
message: `Please enter ${field.label}`,
},
]
: undefined
}
tooltip={field.tooltip}
initialValue={field.default_value}
>
{field.field_type === "password" ? (
<AntInput.Password placeholder={field.placeholder || ""} />
) : field.field_type === "textarea" ? (
<Textarea rows={3} placeholder={field.placeholder || ""} />
) : field.field_type === "select" && field.options ? (
<Select placeholder={field.placeholder || ""}>
{field.options.map((opt) => (
<Select.Option key={opt} value={opt}>
{opt}
</Select.Option>
))}
</Select>
) : (
<Input placeholder={field.placeholder || ""} />
)}
</Form.Item>
),
(field: AgentCredentialFieldMetadata) => {
const error = (formState.errors as any)?.[field.key];
const inputId = `credential-${field.key}`;
return (
<div key={field.key} className="space-y-2">
<Label htmlFor={inputId}>
{field.label}
{field.required && (
<span className="text-destructive"> *</span>
)}
{field.tooltip ? <InfoTip>{field.tooltip}</InfoTip> : null}
</Label>
{field.field_type === "password" ? (
<Input
id={inputId}
type="password"
placeholder={field.placeholder || ""}
aria-invalid={!!error}
{...register(
field.key,
field.required
? { required: `Please enter ${field.label}` }
: undefined,
)}
/>
) : field.field_type === "textarea" ? (
<Textarea
id={inputId}
rows={3}
placeholder={field.placeholder || ""}
aria-invalid={!!error}
{...register(
field.key,
field.required
? { required: `Please enter ${field.label}` }
: undefined,
)}
/>
) : field.field_type === "select" && field.options ? (
<Controller
control={control}
name={field.key}
rules={
field.required
? { required: `Please enter ${field.label}` }
: undefined
}
render={({ field: rhfField }) => (
<Select
value={(rhfField.value as string) ?? ""}
onValueChange={rhfField.onChange}
>
<SelectTrigger id={inputId} aria-invalid={!!error}>
<SelectValue
placeholder={field.placeholder || ""}
/>
</SelectTrigger>
<SelectContent>
{field.options?.map((opt) => (
<SelectItem key={opt} value={opt}>
{opt}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
) : (
<Input
id={inputId}
placeholder={field.placeholder || ""}
aria-invalid={!!error}
{...register(
field.key,
field.required
? { required: `Please enter ${field.label}` }
: undefined,
)}
/>
)}
{error && (
<p className="text-sm text-destructive">
{error.message as string}
</p>
)}
</div>
);
},
)}
<Accordion type="single" collapsible className="mb-4">
@ -99,7 +205,7 @@ const DynamicAgentFormFields: React.FC<DynamicAgentFormFieldsProps> = ({
</AccordionContent>
</AccordionItem>
</Accordion>
</>
</div>
);
};
@ -182,4 +288,3 @@ export const buildDynamicAgentData = (
};
export default DynamicAgentFormFields;