mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
feat(ui): migrate agents subtree to shadcn
Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
550301679c
commit
ae8b10cf8d
6 changed files with 1923 additions and 967 deletions
|
|
@ -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
|
|
@ -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'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'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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 "Edit Settings" to modify agent
|
||||
configuration.
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
</TabPanel>
|
||||
)}
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</form>
|
||||
</FormProvider>
|
||||
) : (
|
||||
<p className="text-muted-foreground">
|
||||
Click "Edit Settings" to modify agent configuration.
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentInfoView;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue