feat(ui): i18n for agent form and card

This commit is contained in:
吹雪 2026-06-11 02:20:53 +09:00
parent aea41b4ada
commit a65632e342
4 changed files with 351 additions and 100 deletions

View file

@ -1,5 +1,6 @@
import React, { useState, useEffect } from "react";
import { Modal, Form, Select, Input, Steps, Radio, Tag, Divider, Switch, InputNumber, Collapse } from "antd";
import { useTranslation } from "react-i18next";
import MessageManager from "@/components/molecules/message_manager";
import { Button } from "@tremor/react";
import { CheckCircleFilled, KeyOutlined, RobotOutlined, AppstoreOutlined, InfoCircleOutlined } from "@ant-design/icons";
@ -40,6 +41,7 @@ interface AddAgentFormProps {
}
const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessToken, onSuccess, teams }) => {
const { t } = useTranslation();
const { userId, userRole } = useAuthorized();
const [form] = Form.useForm();
const [currentStep, setCurrentStep] = useState(0);
@ -244,7 +246,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
const handleCreateAgent = async () => {
if (!accessToken) {
MessageManager.error("No access token available");
MessageManager.error(t("agentsPage.addAgentForm.noAccessToken"));
return;
}
@ -254,7 +256,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
const values = { ...form.getFieldsValue(true) };
const agentData = buildAgentData(values);
if (!agentData) {
MessageManager.error("Failed to build agent data");
MessageManager.error(t("agentsPage.addAgentForm.failedToBuildAgentData"));
setIsSubmitting(false);
return;
}
@ -330,7 +332,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
setCreatedKeyValue(keyResponse.key || null);
} else if (keyAssignOption === "existing_key") {
if (!selectedExistingKey) {
MessageManager.error("Please select an existing key to assign");
MessageManager.error(t("agentsPage.addAgentForm.pleaseSelectExistingKey"));
setIsSubmitting(false);
return;
}
@ -347,7 +349,11 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
} catch (error) {
console.error("Error creating agent:", error);
const errorMessage = error instanceof Error ? error.message : String(error);
MessageManager.error(errorMessage ? `Failed to create agent: ${errorMessage}` : "Failed to create agent");
MessageManager.error(
errorMessage
? t("agentsPage.addAgentForm.failedToCreateAgent", { error: errorMessage })
: t("agentsPage.addAgentForm.failedToCreateAgentGeneric"),
);
} finally {
setIsSubmitting(false);
}
@ -374,20 +380,23 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
const renderEntitlementsStep = () => (
<div className="space-y-4">
<p className="text-sm text-gray-600">
Configure which models, agents, and MCP tools this agent is allowed to use. Leave fields empty to allow all
(subject to key/team permissions).
</p>
<p className="text-sm text-gray-600">{t("agentsPage.addAgentForm.entitlementsIntro")}</p>
<Form.Item
label={<span className="text-sm font-medium text-gray-700">Allowed Models</span>}
label={
<span className="text-sm font-medium text-gray-700">{t("agentsPage.addAgentForm.allowedModelsLabel")}</span>
}
name="entitlement_models"
tooltip="Restrict which models this agent can call. Leave empty to allow all."
tooltip={t("agentsPage.addAgentForm.allowedModelsTooltip")}
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder={loadingModels ? "Loading models..." : "Select models (leave empty for all)"}
placeholder={
loadingModels
? t("agentsPage.addAgentForm.loadingModels")
: t("agentsPage.addAgentForm.allowedModelsPlaceholder")
}
tokenSeparators={[","]}
loading={loadingModels}
showSearch
@ -399,14 +408,20 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
</Form.Item>
<Form.Item
label={<span className="text-sm font-medium text-gray-700">Allowed Agents (Sub-Agents)</span>}
label={
<span className="text-sm font-medium text-gray-700">{t("agentsPage.addAgentForm.allowedAgentsLabel")}</span>
}
name="entitlement_agents"
tooltip="Restrict which other agents this agent can invoke as sub-agents. Leave empty to allow all."
tooltip={t("agentsPage.addAgentForm.allowedAgentsTooltip")}
>
<Select
mode="multiple"
style={{ width: "100%" }}
placeholder={loadingAgents ? "Loading agents..." : "Select agents (leave empty for all)"}
placeholder={
loadingAgents
? t("agentsPage.addAgentForm.loadingAgents")
: t("agentsPage.addAgentForm.allowedAgentsPlaceholder")
}
loading={loadingAgents}
showSearch
filterOption={(input, option) =>
@ -424,9 +439,9 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
<Form.Item
label={
<span>
Allowed MCP Servers{" "}
{t("agentsPage.addAgentForm.allowedMcpServersLabel")}{" "}
<InfoCircleOutlined
title="Select which MCP servers or access groups this agent can access"
title={t("agentsPage.addAgentForm.allowedMcpServersTooltip")}
style={{ marginLeft: "4px" }}
/>
</span>
@ -440,7 +455,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
}
value={form.getFieldValue("allowed_mcp_servers_and_groups") || { servers: [], accessGroups: [] }}
accessToken={accessToken ?? ""}
placeholder="Select MCP servers or access groups (optional)"
placeholder={t("agentsPage.addAgentForm.mcpServerSelectorPlaceholder")}
/>
</Form.Item>
<Form.Item name="mcp_tool_permissions" initialValue={{}} hidden>
@ -472,16 +487,14 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
const renderObservabilityStep = () => (
<div className="space-y-6">
<div>
<h4 className="text-sm font-medium text-gray-700 mb-3">Tracing</h4>
<h4 className="text-sm font-medium text-gray-700 mb-3">{t("agentsPage.addAgentForm.tracingTitle")}</h4>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<span className="text-sm font-medium text-gray-700">
Require x-litellm-trace-id on calls TO this agent
{t("agentsPage.addAgentForm.requireTraceIdInboundLabel")}
</span>
<p className="text-xs text-gray-500 mt-1">
Only accept this agent being invoked with a trace-id (e.g. when used as a sub-agent).
</p>
<p className="text-xs text-gray-500 mt-1">{t("agentsPage.addAgentForm.requireTraceIdInboundHint")}</p>
</div>
<Switch checked={requireTraceIdInbound} onChange={setRequireTraceIdInbound} />
</div>
@ -489,11 +502,9 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
<div className="flex items-center justify-between">
<div>
<span className="text-sm font-medium text-gray-700">
Require x-litellm-trace-id on calls BY this agent
{t("agentsPage.addAgentForm.requireTraceIdOutboundLabel")}
</span>
<p className="text-xs text-gray-500 mt-1">
Requires LLM/MCP calls made by this agent to include x-litellm-trace-id for session tracking.
</p>
<p className="text-xs text-gray-500 mt-1">{t("agentsPage.addAgentForm.requireTraceIdOutboundHint")}</p>
</div>
<Switch
checked={requireTraceIdOutbound}
@ -512,67 +523,98 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
<Divider className="my-0" />
<div>
<h4 className="text-sm font-medium text-gray-700 mb-3">Budgets &amp; Rate Limits</h4>
<h4 className="text-sm font-medium text-gray-700 mb-3">{t("agentsPage.addAgentForm.budgetsTitle")}</h4>
<div className="space-y-4">
{!requireTraceIdOutbound && (
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg text-sm text-yellow-800">
Enable &quot;Require x-litellm-trace-id on calls BY this agent&quot; in Tracing to configure budgets and
rate limits.
{t("agentsPage.addAgentForm.enableTraceIdWarning")}
</div>
)}
<div className="text-sm font-medium text-gray-700">Session Budgets</div>
<div className="text-sm font-medium text-gray-700">{t("agentsPage.addAgentForm.sessionBudgetsTitle")}</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm text-gray-600 block mb-1">Max Iterations</label>
<label className="text-sm text-gray-600 block mb-1">
{t("agentsPage.addAgentForm.maxIterationsLabel")}
</label>
<InputNumber
className="w-full"
min={1}
placeholder="e.g. 25"
placeholder={t("agentsPage.addAgentForm.maxIterationsPlaceholder")}
disabled={!requireTraceIdOutbound}
value={maxIterations}
onChange={(val) => setMaxIterations(val)}
/>
<p className="text-xs text-gray-400 mt-1">Hard cap on LLM calls per session</p>
<p className="text-xs text-gray-400 mt-1">{t("agentsPage.addAgentForm.maxIterationsHint")}</p>
</div>
<div>
<label className="text-sm text-gray-600 block mb-1">Max Budget Per Session ($)</label>
<label className="text-sm text-gray-600 block mb-1">
{t("agentsPage.addAgentForm.maxBudgetPerSessionLabel")}
</label>
<InputNumber
className="w-full"
min={0.01}
step={0.5}
placeholder="e.g. 5.00"
placeholder={t("agentsPage.addAgentForm.maxBudgetPerSessionPlaceholder")}
disabled={!requireTraceIdOutbound}
value={maxBudgetPerSession}
onChange={(val) => setMaxBudgetPerSession(val)}
/>
<p className="text-xs text-gray-400 mt-1">Max spend per trace before returning 429</p>
<p className="text-xs text-gray-400 mt-1">{t("agentsPage.addAgentForm.maxBudgetPerSessionHint")}</p>
</div>
</div>
<Divider className="my-2" />
<div className="text-sm font-medium text-gray-700">Agent Rate Limits</div>
<p className="text-xs text-gray-500">Global rate limits applied across all callers of this agent.</p>
<div className="text-sm font-medium text-gray-700">{t("agentsPage.addAgentForm.agentRateLimitsTitle")}</div>
<p className="text-xs text-gray-500">{t("agentsPage.addAgentForm.agentRateLimitsHint")}</p>
<div className="grid grid-cols-2 gap-4">
<Form.Item label="TPM Limit" name="tpm_limit" className="mb-0">
<InputNumber className="w-full" min={0} placeholder="e.g. 100000" disabled={!requireTraceIdOutbound} />
<Form.Item label={t("agentsPage.addAgentForm.tpmLimitLabel")} name="tpm_limit" className="mb-0">
<InputNumber
className="w-full"
min={0}
placeholder={t("agentsPage.addAgentForm.tpmLimitPlaceholder")}
disabled={!requireTraceIdOutbound}
/>
</Form.Item>
<Form.Item label="RPM Limit" name="rpm_limit" className="mb-0">
<InputNumber className="w-full" min={0} placeholder="e.g. 100" disabled={!requireTraceIdOutbound} />
<Form.Item label={t("agentsPage.addAgentForm.rpmLimitLabel")} name="rpm_limit" className="mb-0">
<InputNumber
className="w-full"
min={0}
placeholder={t("agentsPage.addAgentForm.rpmLimitPlaceholder")}
disabled={!requireTraceIdOutbound}
/>
</Form.Item>
</div>
<div className="text-sm font-medium text-gray-700 mt-4">Per-Session Rate Limits</div>
<p className="text-xs text-gray-500">
Rate limits per session (x-litellm-trace-id). Each session gets its own counters.
</p>
<div className="text-sm font-medium text-gray-700 mt-4">
{t("agentsPage.addAgentForm.perSessionRateLimitsTitle")}
</div>
<p className="text-xs text-gray-500">{t("agentsPage.addAgentForm.perSessionRateLimitsHint")}</p>
<div className="grid grid-cols-2 gap-4">
<Form.Item label="Session TPM Limit" name="session_tpm_limit" className="mb-0">
<InputNumber className="w-full" min={0} placeholder="e.g. 10000" disabled={!requireTraceIdOutbound} />
<Form.Item
label={t("agentsPage.addAgentForm.sessionTpmLimitLabel")}
name="session_tpm_limit"
className="mb-0"
>
<InputNumber
className="w-full"
min={0}
placeholder={t("agentsPage.addAgentForm.sessionTpmLimitPlaceholder")}
disabled={!requireTraceIdOutbound}
/>
</Form.Item>
<Form.Item label="Session RPM Limit" name="session_rpm_limit" className="mb-0">
<InputNumber className="w-full" min={0} placeholder="e.g. 20" disabled={!requireTraceIdOutbound} />
<Form.Item
label={t("agentsPage.addAgentForm.sessionRpmLimitLabel")}
name="session_rpm_limit"
className="mb-0"
>
<InputNumber
className="w-full"
min={0}
placeholder={t("agentsPage.addAgentForm.sessionRpmLimitPlaceholder")}
disabled={!requireTraceIdOutbound}
/>
</Form.Item>
</div>
</div>
@ -581,10 +623,8 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
<Divider className="my-0" />
<div>
<h4 className="text-sm font-medium text-gray-700 mb-3">Guardrails</h4>
<p className="text-xs text-gray-500 mb-3">
Apply guardrails to this agent. Selected guardrails will run on all calls made by this agent.
</p>
<h4 className="text-sm font-medium text-gray-700 mb-3">{t("agentsPage.addAgentForm.guardrailsTitle")}</h4>
<p className="text-xs text-gray-500 mb-3">{t("agentsPage.addAgentForm.guardrailsHint")}</p>
<Form.Item name="guardrails" initialValue={[]}>
<GuardrailSelector
accessToken={accessToken ?? ""}
@ -666,9 +706,9 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
const renderConfigureStep = () => (
<>
<Form.Item
label={<span className="text-sm font-medium text-gray-700">Agent Type</span>}
label={<span className="text-sm font-medium text-gray-700">{t("agentsPage.addAgentForm.agentTypeLabel")}</span>}
required
tooltip="Select the type of agent you want to create"
tooltip={t("agentsPage.addAgentForm.agentTypeTooltip")}
>
<Select
value={agentType}
@ -681,7 +721,9 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
{menu}
<Divider style={{ margin: "4px 0" }} />
<div className="px-2 py-1">
<div className="text-xs text-gray-400 font-medium mb-1 uppercase tracking-wide px-2">Not listed?</div>
<div className="text-xs text-gray-400 font-medium mb-1 uppercase tracking-wide px-2">
{t("agentsPage.addAgentForm.notListedLabel")}
</div>
<div
className={`flex items-center gap-3 px-2 py-2 rounded cursor-pointer transition-colors ${
agentType === CUSTOM_AGENT_TYPE ? "bg-amber-50" : "hover:bg-amber-50"
@ -691,14 +733,14 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
<AppstoreOutlined className="text-amber-600 text-lg" />
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-amber-700">Custom / Other</span>
<span className="font-medium text-amber-700">
{t("agentsPage.addAgentForm.customAgentLabel")}
</span>
<Tag color="orange" style={{ fontSize: 10, padding: "0 4px" }}>
GENERIC
</Tag>
</div>
<div className="text-xs text-amber-600">
For agents that don&apos;t follow a standard protocol — just needs a virtual key
</div>
<div className="text-xs text-amber-600">{t("agentsPage.addAgentForm.customAgentDescription")}</div>
</div>
</div>
</div>
@ -732,14 +774,14 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
{agentType === CUSTOM_AGENT_TYPE ? (
<div className="space-y-4">
<Form.Item
label="Agent Name"
label={t("agentsPage.addAgentForm.agentNameLabel")}
name="agent_name"
rules={[{ required: true, message: "Please enter an agent name" }]}
rules={[{ required: true, message: t("agentsPage.addAgentForm.agentNameRequired") }]}
>
<Input placeholder="e.g. my-custom-agent" />
<Input placeholder={t("agentsPage.addAgentForm.agentNamePlaceholder")} />
</Form.Item>
<Form.Item label="Description" name="description">
<Input.TextArea placeholder="Describe what this agent does…" rows={3} />
<Form.Item label={t("common.description")} name="description">
<Input.TextArea placeholder={t("agentsPage.addAgentForm.descriptionPlaceholder")} rows={3} />
</Form.Item>
</div>
) : agentType === "a2a" ? (
@ -757,7 +799,16 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
key={field.key}
label={field.label}
name={field.key}
rules={field.required ? [{ required: true, message: `Please enter ${field.label}` }] : undefined}
rules={
field.required
? [
{
required: true,
message: t("agentsPage.addAgentForm.credentialFieldRequired", { label: field.label }),
},
]
: undefined
}
tooltip={field.tooltip}
initialValue={field.default_value}
>
@ -805,9 +856,11 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
</div>
<Form.Item
label={<span className="text-sm font-medium text-gray-700">Assign to Team</span>}
label={
<span className="text-sm font-medium text-gray-700">{t("agentsPage.addAgentForm.assignToTeamLabel")}</span>
}
name="team_id"
tooltip="Optionally assign this agent to a team. The agent and its key will belong to the selected team."
tooltip={t("agentsPage.addAgentForm.assignToTeamTooltip")}
>
<TeamDropdown />
</Form.Item>
@ -834,24 +887,26 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
<div className="flex-1">
<div className="flex items-center gap-2">
<KeyOutlined className="text-indigo-600" />
<span className="font-medium text-gray-900">Create a new key for this agent</span>
<span className="font-medium text-gray-900">{t("agentsPage.addAgentForm.createNewKeyLabel")}</span>
</div>
<p className="text-sm text-gray-500 mt-1">A dedicated key scoped to this agent.</p>
<p className="text-sm text-gray-500 mt-1">{t("agentsPage.addAgentForm.createNewKeyDescription")}</p>
{keyAssignOption === "create_new" && (
<div className="mt-3 space-y-3" onClick={(e) => e.stopPropagation()}>
<div>
<label className="text-sm text-gray-600 block mb-1">Key Name</label>
<label className="text-sm text-gray-600 block mb-1">
{t("agentsPage.addAgentForm.keyNameLabel")}
</label>
<Input
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
placeholder="e.g. my-agent-key"
placeholder={t("agentsPage.addAgentForm.keyNamePlaceholder")}
/>
</div>
</div>
)}
</div>
</div>
<Tag color="green">Recommended</Tag>
<Tag color="green">{t("agentsPage.addAgentForm.recommended")}</Tag>
</div>
</div>
@ -873,15 +928,19 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
<div className="flex-1">
<div className="flex items-center gap-2">
<KeyOutlined className="text-gray-500" />
<span className="font-medium text-gray-900">Assign an existing key</span>
<span className="font-medium text-gray-900">
{t("agentsPage.addAgentForm.assignExistingKeyLabel")}
</span>
</div>
<p className="text-sm text-gray-500 mt-1">Re-assign a key you already have to this agent.</p>
<p className="text-sm text-gray-500 mt-1">
{t("agentsPage.addAgentForm.assignExistingKeyDescription")}
</p>
{keyAssignOption === "existing_key" && (
<div className="mt-3" onClick={(e) => e.stopPropagation()}>
<Select
showSearch
style={{ width: "100%" }}
placeholder="Search by key name…"
placeholder={t("agentsPage.addAgentForm.searchByKeyName")}
loading={loadingKeys}
value={selectedExistingKey}
onChange={(value) => setSelectedExistingKey(value)}
@ -906,7 +965,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
className="text-sm text-gray-500 underline hover:text-gray-700"
onClick={() => setKeyAssignOption("skip")}
>
Skip for now — I&apos;ll assign a key later
{t("agentsPage.addAgentForm.skipKeyAssign")}
</button>
</div>
</div>
@ -916,7 +975,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
const renderReadyStep = () => (
<div className="text-center py-6">
<CheckCircleFilled className="text-5xl text-green-500 mb-4" style={{ fontSize: 48 }} />
<h3 className="text-xl font-semibold text-gray-900 mb-2">Agent Created!</h3>
<h3 className="text-xl font-semibold text-gray-900 mb-2">{t("agentsPage.addAgentForm.agentCreatedTitle")}</h3>
<div className="flex justify-center mb-4">
<Tag icon={<RobotOutlined />} color="purple" className="px-3 py-1 text-sm">
{createdAgentName}
@ -929,11 +988,11 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
)}
{assignedKeyAlias && (
<p className="text-sm text-gray-600 mt-2">
Key <span className="font-medium">{assignedKeyAlias}</span> has been assigned to this agent.
{t("agentsPage.addAgentForm.keyAssignedMessage", { keyAlias: assignedKeyAlias })}
</p>
)}
{!createdKeyValue && !assignedKeyAlias && keyAssignOption === "skip" && (
<p className="text-sm text-gray-500 mt-2">No key assigned. You can create one from the Virtual Keys page.</p>
<p className="text-sm text-gray-500 mt-2">{t("agentsPage.addAgentForm.noKeyAssigned")}</p>
)}
</div>
);
@ -943,7 +1002,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
title={
<div className="flex items-center space-x-3 pb-4 border-b border-gray-100">
{selectedLogo && currentStep < 1 && <img src={selectedLogo} alt="Agent" className="w-6 h-6 object-contain" />}
<h2 className="text-xl font-semibold text-gray-900">Add New Agent</h2>
<h2 className="text-xl font-semibold text-gray-900">{t("agentsPage.addAgentForm.modalTitle")}</h2>
</div>
}
open={visible}
@ -959,11 +1018,11 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
<div className="mt-4">
{/* Step indicator */}
<Steps current={currentStep} size="small" className="mb-8">
<Step title="Configure" />
<Step title="Entitlements" />
<Step title="Governance" />
<Step title="Agent Management" />
<Step title="Ready" />
<Step title={t("agentsPage.addAgentForm.stepConfigure")} />
<Step title={t("agentsPage.addAgentForm.stepEntitlements")} />
<Step title={t("agentsPage.addAgentForm.stepGovernance")} />
<Step title={t("agentsPage.addAgentForm.stepAgentManagement")} />
<Step title={t("agentsPage.addAgentForm.stepReady")} />
</Steps>
<Form
@ -1005,39 +1064,41 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
onClick={handleBack}
className="text-sm text-gray-600 border border-gray-300 rounded px-4 py-2 hover:bg-gray-50"
>
← Back
{t("agentsPage.addAgentForm.backButton")}
</button>
)}
</div>
<div className="flex gap-3">
{currentStep < 4 && (
<Button variant="secondary" onClick={handleClose}>
Cancel
{t("common.cancel")}
</Button>
)}
{currentStep === 0 && (
<Button variant="primary" onClick={handleNext}>
Next →
{t("agentsPage.addAgentForm.nextButton")}
</Button>
)}
{currentStep === 1 && (
<Button variant="primary" onClick={handleNext}>
Next →
{t("agentsPage.addAgentForm.nextButton")}
</Button>
)}
{currentStep === 2 && (
<Button variant="primary" onClick={handleNext}>
Next →
{t("agentsPage.addAgentForm.nextButton")}
</Button>
)}
{currentStep === 3 && (
<Button variant="primary" loading={isSubmitting} onClick={handleCreateAgent}>
{isSubmitting ? "Creating..." : "Create Agent →"}
{isSubmitting
? t("agentsPage.addAgentForm.creatingButton")
: t("agentsPage.addAgentForm.createAgentButton")}
</Button>
)}
{currentStep === 4 && (
<Button variant="primary" onClick={handleClose}>
Done
{t("common.done")}
</Button>
)}
</div>

View file

@ -1,6 +1,7 @@
import React from "react";
import { Card, Badge, Tooltip, Button } from "antd";
import { CopyOutlined, KeyOutlined, WarningOutlined, DeleteOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { Agent, AgentKeyInfo } from "./types";
interface AgentCardProps {
@ -14,10 +15,15 @@ interface AgentCardProps {
}
const AgentCard: React.FC<AgentCardProps> = ({ agent, keyInfo, onAgentClick, onDeleteClick, isAdmin }) => {
const description = agent.agent_card_params?.description || "No description";
const { t } = useTranslation();
const description = agent.agent_card_params?.description || t("agentsPage.agentCard.noDescription");
const url = agent.agent_card_params?.url;
const hasKey = keyInfo?.has_key ?? false;
const statusBadge = hasKey ? <Badge status="success" text="Active" /> : <Badge status="warning" text="Needs Setup" />;
const statusBadge = hasKey ? (
<Badge status="success" text={t("agentsPage.agentCard.statusActive")} />
) : (
<Badge status="warning" text={t("agentsPage.agentCard.statusNeedsSetup")} />
);
const copyToClipboard = (e: React.MouseEvent, text: string) => {
e.stopPropagation();
@ -37,7 +43,7 @@ const AgentCard: React.FC<AgentCardProps> = ({ agent, keyInfo, onAgentClick, onD
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium text-gray-900 truncate">{agent.agent_name}</span>
<Tooltip title="Copy Agent ID">
<Tooltip title={t("agentsPage.agentCard.copyAgentId")}>
<CopyOutlined
onClick={(e) => copyToClipboard(e, agent.agent_id)}
className="cursor-pointer text-gray-400 hover:text-blue-500 text-xs shrink-0"
@ -47,7 +53,7 @@ const AgentCard: React.FC<AgentCardProps> = ({ agent, keyInfo, onAgentClick, onD
<div className="mt-1">{statusBadge}</div>
</div>
{isAdmin && onDeleteClick && (
<Tooltip title="Delete agent">
<Tooltip title={t("agentsPage.agentCard.deleteAgent")}>
<Button
type="text"
size="small"
@ -72,12 +78,12 @@ const AgentCard: React.FC<AgentCardProps> = ({ agent, keyInfo, onAgentClick, onD
{hasKey ? (
<div className="flex items-center gap-1.5 text-gray-600">
<KeyOutlined />
<span>{keyInfo?.key_alias || keyInfo?.token_prefix || "Key assigned"}</span>
<span>{keyInfo?.key_alias || keyInfo?.token_prefix || t("agentsPage.agentCard.keyAssigned")}</span>
</div>
) : (
<div className="flex items-center gap-1.5 text-amber-600">
<WarningOutlined />
<span>No key assigned</span>
<span>{t("agentsPage.agentCard.noKeyAssigned")}</span>
</div>
)}
</div>

View file

@ -5942,5 +5942,97 @@
"updateFailed": "Error updating vector store: {{error}}",
"fetchDetailsFailed": "Error fetching vector store details: {{error}}"
}
},
"agentsPage": {
"addAgentForm": {
"noAccessToken": "No access token available",
"failedToBuildAgentData": "Failed to build agent data",
"pleaseSelectExistingKey": "Please select an existing key to assign",
"failedToCreateAgent": "Failed to create agent: {{error}}",
"failedToCreateAgentGeneric": "Failed to create agent",
"allowedModelsLabel": "Allowed Models",
"allowedModelsTooltip": "Restrict which models this agent can call. Leave empty to allow all.",
"allowedModelsPlaceholder": "Select models (leave empty for all)",
"loadingModels": "Loading models...",
"allowedAgentsLabel": "Allowed Agents (Sub-Agents)",
"allowedAgentsTooltip": "Restrict which other agents this agent can invoke as sub-agents. Leave empty to allow all.",
"allowedAgentsPlaceholder": "Select agents (leave empty for all)",
"loadingAgents": "Loading agents...",
"allowedMcpServersLabel": "Allowed MCP Servers",
"allowedMcpServersTooltip": "Select which MCP servers or access groups this agent can access",
"mcpServerSelectorPlaceholder": "Select MCP servers or access groups (optional)",
"entitlementsIntro": "Configure which models, agents, and MCP tools this agent is allowed to use. Leave fields empty to allow all (subject to key/team permissions).",
"tracingTitle": "Tracing",
"requireTraceIdInboundLabel": "Require x-litellm-trace-id on calls TO this agent",
"requireTraceIdInboundHint": "Only accept this agent being invoked with a trace-id (e.g. when used as a sub-agent).",
"requireTraceIdOutboundLabel": "Require x-litellm-trace-id on calls BY this agent",
"requireTraceIdOutboundHint": "Requires LLM/MCP calls made by this agent to include x-litellm-trace-id for session tracking.",
"budgetsTitle": "Budgets & Rate Limits",
"enableTraceIdWarning": "Enable \"Require x-litellm-trace-id on calls BY this agent\" in Tracing to configure budgets and rate limits.",
"sessionBudgetsTitle": "Session Budgets",
"maxIterationsLabel": "Max Iterations",
"maxIterationsPlaceholder": "e.g. 25",
"maxIterationsHint": "Hard cap on LLM calls per session",
"maxBudgetPerSessionLabel": "Max Budget Per Session ($)",
"maxBudgetPerSessionPlaceholder": "e.g. 5.00",
"maxBudgetPerSessionHint": "Max spend per trace before returning 429",
"agentRateLimitsTitle": "Agent Rate Limits",
"agentRateLimitsHint": "Global rate limits applied across all callers of this agent.",
"tpmLimitLabel": "TPM Limit",
"tpmLimitPlaceholder": "e.g. 100000",
"rpmLimitLabel": "RPM Limit",
"rpmLimitPlaceholder": "e.g. 100",
"perSessionRateLimitsTitle": "Per-Session Rate Limits",
"perSessionRateLimitsHint": "Rate limits per session (x-litellm-trace-id). Each session gets its own counters.",
"sessionTpmLimitLabel": "Session TPM Limit",
"sessionTpmLimitPlaceholder": "e.g. 10000",
"sessionRpmLimitLabel": "Session RPM Limit",
"sessionRpmLimitPlaceholder": "e.g. 20",
"guardrailsTitle": "Guardrails",
"guardrailsHint": "Apply guardrails to this agent. Selected guardrails will run on all calls made by this agent.",
"agentTypeLabel": "Agent Type",
"agentTypeTooltip": "Select the type of agent you want to create",
"notListedLabel": "Not listed?",
"customAgentLabel": "Custom / Other",
"customAgentDescription": "For agents that don't follow a standard protocol — just needs a virtual key",
"agentNameLabel": "Agent Name",
"agentNameRequired": "Please enter an agent name",
"agentNamePlaceholder": "e.g. my-custom-agent",
"descriptionPlaceholder": "Describe what this agent does…",
"assignToTeamLabel": "Assign to Team",
"assignToTeamTooltip": "Optionally assign this agent to a team. The agent and its key will belong to the selected team.",
"createNewKeyLabel": "Create a new key for this agent",
"createNewKeyDescription": "A dedicated key scoped to this agent.",
"keyNameLabel": "Key Name",
"keyNamePlaceholder": "e.g. my-agent-key",
"recommended": "Recommended",
"assignExistingKeyLabel": "Assign an existing key",
"assignExistingKeyDescription": "Re-assign a key you already have to this agent.",
"searchByKeyName": "Search by key name…",
"skipKeyAssign": "Skip for now — I'll assign a key later",
"agentCreatedTitle": "Agent Created!",
"keyAssignedMessage": "Key {{keyAlias}} has been assigned to this agent.",
"noKeyAssigned": "No key assigned. You can create one from the Virtual Keys page.",
"modalTitle": "Add New Agent",
"stepConfigure": "Configure",
"stepEntitlements": "Entitlements",
"stepGovernance": "Governance",
"stepAgentManagement": "Agent Management",
"stepReady": "Ready",
"backButton": "← Back",
"nextButton": "Next →",
"createAgentButton": "Create Agent →",
"creatingButton": "Creating...",
"credentialFieldRequired": "Please enter {{label}}"
},
"agentCard": {
"noDescription": "No description",
"copyAgentId": "Copy Agent ID",
"deleteAgent": "Delete agent",
"keyAssigned": "Key assigned",
"noKeyAssigned": "No key assigned",
"statusActive": "Active",
"statusNeedsSetup": "Needs Setup"
}
}
}

View file

@ -5942,5 +5942,97 @@
"updateFailed": "更新向量存储失败:{{error}}",
"fetchDetailsFailed": "获取向量存储详情失败:{{error}}"
}
},
"agentsPage": {
"addAgentForm": {
"noAccessToken": "无可用访问令牌",
"failedToBuildAgentData": "构建 Agent 数据失败",
"pleaseSelectExistingKey": "请选择要分配的现有密钥",
"failedToCreateAgent": "创建 Agent 失败:{{error}}",
"failedToCreateAgentGeneric": "创建 Agent 失败",
"allowedModelsLabel": "允许的模型",
"allowedModelsTooltip": "限制此 Agent 可调用的模型。留空表示允许所有模型。",
"allowedModelsPlaceholder": "选择模型(留空表示允许所有)",
"loadingModels": "加载模型中...",
"allowedAgentsLabel": "允许的 Agent(子 Agent)",
"allowedAgentsTooltip": "限制此 Agent 可作为子 Agent 调用的其他 Agent。留空表示允许所有。",
"allowedAgentsPlaceholder": "选择 Agent(留空表示允许所有)",
"loadingAgents": "加载 Agent 中...",
"allowedMcpServersLabel": "允许的 MCP 服务器",
"allowedMcpServersTooltip": "选择此 Agent 可访问的 MCP 服务器或访问组",
"mcpServerSelectorPlaceholder": "选择 MCP 服务器或访问组(可选)",
"entitlementsIntro": "配置此 Agent 允许使用的模型、Agent 和 MCP 工具。留空表示允许所有(受密钥/团队权限约束)。",
"tracingTitle": "追踪",
"requireTraceIdInboundLabel": "要求调用此 Agent 时携带 x-litellm-trace-id",
"requireTraceIdInboundHint": "仅接受携带 trace-id 的调用(例如作为子 Agent 使用时)。",
"requireTraceIdOutboundLabel": "要求此 Agent 发出的调用携带 x-litellm-trace-id",
"requireTraceIdOutboundHint": "要求此 Agent 发出的 LLM/MCP 调用包含 x-litellm-trace-id 以进行会话追踪。",
"budgetsTitle": "预算与速率限制",
"enableTraceIdWarning": "请在「追踪」中启用「要求此 Agent 发出的调用携带 x-litellm-trace-id」以配置预算和速率限制。",
"sessionBudgetsTitle": "会话预算",
"maxIterationsLabel": "最大迭代次数",
"maxIterationsPlaceholder": "例如 25",
"maxIterationsHint": "每个会话的 LLM 调用硬性上限",
"maxBudgetPerSessionLabel": "每会话最大预算($)",
"maxBudgetPerSessionPlaceholder": "例如 5.00",
"maxBudgetPerSessionHint": "每个 trace 返回 429 前的最大花费",
"agentRateLimitsTitle": "Agent 速率限制",
"agentRateLimitsHint": "对所有调用此 Agent 的请求方全局生效的速率限制。",
"tpmLimitLabel": "TPM 限制",
"tpmLimitPlaceholder": "例如 100000",
"rpmLimitLabel": "RPM 限制",
"rpmLimitPlaceholder": "例如 100",
"perSessionRateLimitsTitle": "每会话速率限制",
"perSessionRateLimitsHint": "按会话(x-litellm-trace-id)设置的速率限制,每个会话独立计数。",
"sessionTpmLimitLabel": "会话 TPM 限制",
"sessionTpmLimitPlaceholder": "例如 10000",
"sessionRpmLimitLabel": "会话 RPM 限制",
"sessionRpmLimitPlaceholder": "例如 20",
"guardrailsTitle": "护栏",
"guardrailsHint": "为此 Agent 应用护栏。选定的护栏将对此 Agent 的所有调用生效。",
"agentTypeLabel": "Agent 类型",
"agentTypeTooltip": "选择要创建的 Agent 类型",
"notListedLabel": "没有找到?",
"customAgentLabel": "自定义 / 其他",
"customAgentDescription": "适用于不遵循标准协议的 Agent,只需一个虚拟密钥",
"agentNameLabel": "Agent 名称",
"agentNameRequired": "请输入 Agent 名称",
"agentNamePlaceholder": "例如 my-custom-agent",
"descriptionPlaceholder": "描述此 Agent 的功能…",
"assignToTeamLabel": "分配到团队",
"assignToTeamTooltip": "可选:将此 Agent 分配到团队。Agent 及其密钥将归属于所选团队。",
"createNewKeyLabel": "为此 Agent 创建新密钥",
"createNewKeyDescription": "专用于此 Agent 的密钥。",
"keyNameLabel": "密钥名称",
"keyNamePlaceholder": "例如 my-agent-key",
"recommended": "推荐",
"assignExistingKeyLabel": "分配现有密钥",
"assignExistingKeyDescription": "将已有密钥重新分配给此 Agent。",
"searchByKeyName": "按密钥名称搜索…",
"skipKeyAssign": "暂时跳过,稍后再分配密钥",
"agentCreatedTitle": "Agent 已创建!",
"keyAssignedMessage": "密钥 {{keyAlias}} 已分配给此 Agent。",
"noKeyAssigned": "未分配密钥。您可以在虚拟密钥页面创建密钥。",
"modalTitle": "添加新 Agent",
"stepConfigure": "配置",
"stepEntitlements": "权限",
"stepGovernance": "治理",
"stepAgentManagement": "Agent 管理",
"stepReady": "完成",
"backButton": "← 返回",
"nextButton": "下一步 →",
"createAgentButton": "创建 Agent →",
"creatingButton": "创建中...",
"credentialFieldRequired": "请输入 {{label}}"
},
"agentCard": {
"noDescription": "无描述",
"copyAgentId": "复制 Agent ID",
"deleteAgent": "删除 Agent",
"keyAssigned": "已分配密钥",
"noKeyAssigned": "未分配密钥",
"statusActive": "活跃",
"statusNeedsSetup": "待配置"
}
}
}