| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708 |
- import * as React from "react";
- import { useTranslation } from "react-i18next";
- import {
- Bot,
- RefreshCw,
- Search,
- Sparkles,
- Trash2,
- } from "lucide-react";
- import { createAgentConfig, deleteAgent, listAgentConfigs, listAgentRuns, listModels, listSkills, updateAgent } from "@/api";
- import { translateApiError } from "@/api/errors";
- import { ApiErrorState } from "@/components/shared/ApiErrorState";
- import { ConfirmDialog } from "@/components/shared/ConfirmDialog";
- import { EmptyState } from "@/components/shared/EmptyState";
- import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
- import { PageHeader } from "@/components/shared/PageHeader";
- import { SearchInput } from "@/components/shared/SearchInput";
- import { Badge } from "@/components/ui/badge";
- import { Button } from "@/components/ui/button";
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
- import { Dialog } from "@/components/ui/dialog";
- import { Input, Textarea } from "@/components/ui/input";
- import { Select } from "@/components/ui/select";
- import { toast } from "@/components/ui/toaster";
- import { demoText } from "@/lib/demo-text";
- import { useAgentList } from "@/hooks";
- import type { AgentConfig, AgentDefinition, AgentRun, JSONObject, ModelDefinition, SkillDefinition } from "@/types";
- import { AgentOverview } from "./components/AgentOverview";
- import { AgentRuns } from "./components/AgentRuns";
- import { CreateAgentDialog } from "./components/CreateAgentDialog";
- export function AgentListPage() {
- const { t } = useTranslation();
- const [search, setSearch] = React.useState("");
- const [selectedAgentId, setSelectedAgentId] = React.useState<string>();
- const [configs, setConfigs] = React.useState<AgentConfig[]>([]);
- const [runs, setRuns] = React.useState<AgentRun[]>([]);
- const [skills, setSkills] = React.useState<SkillDefinition[]>([]);
- const [relatedLoading, setRelatedLoading] = React.useState(true);
- const [directoryLoading, setDirectoryLoading] = React.useState(false);
- const [editOpen, setEditOpen] = React.useState(false);
- const [deleteOpen, setDeleteOpen] = React.useState(false);
- const [detailTab, setDetailTab] = React.useState<"overview" | "test">("overview");
- const agents = useAgentList();
- const agentList = agents.data ?? [];
- const selectedAgent = agentList.find((agent) => agent.id === selectedAgentId) ?? agentList[0];
- const selectedConfigs = configs.filter((config) => config.agent_id === selectedAgent?.id);
- const selectedRuns = runs
- .filter((r) => r.agent_id === selectedAgent?.id)
- .sort((a, b) => new Date(b.created_time).getTime() - new Date(a.created_time).getTime());
- const activeConfig = [...selectedConfigs].sort((a, b) => new Date(b.created_time).getTime() - new Date(a.created_time).getTime())[0];
- const configByAgent = React.useMemo(() => {
- const grouped = new Map<string, AgentConfig[]>();
- configs.forEach((config) => {
- grouped.set(config.agent_id, [...(grouped.get(config.agent_id) ?? []), config]);
- });
- const result = new Map<string, AgentConfig>();
- grouped.forEach((items, agentId) => {
- const config = [...items].sort((a, b) => new Date(b.created_time).getTime() - new Date(a.created_time).getTime())[0];
- if (config) result.set(agentId, config);
- });
- return result;
- }, [configs]);
- const runsByAgent = React.useMemo(() => {
- const result = new Map<string, AgentRun[]>();
- runs.forEach((run) => {
- result.set(run.agent_id, [...(result.get(run.agent_id) ?? []), run]);
- });
- return result;
- }, [runs]);
- const filtered = agentList.filter((agent) => {
- const text = `${agent.name} ${agent.description ?? ""}`.toLowerCase();
- return text.includes(search.toLowerCase());
- });
- const loadSkills = React.useCallback(async () => {
- try {
- setSkills(await listSkills());
- } catch {
- setSkills([]);
- }
- }, []);
- const loadRelated = React.useCallback(async (agentId?: string, options?: { silent?: boolean }) => {
- if (!agentId) {
- setConfigs([]);
- setRuns([]);
- setRelatedLoading(false);
- return;
- }
- if (!options?.silent) setRelatedLoading(true);
- try {
- const [configData, runData] = await Promise.all([listAgentConfigs(agentId), listAgentRuns(agentId)]);
- setConfigs((current) => [...current.filter((config) => config.agent_id !== agentId), ...configData]);
- setRuns((current) => [...current.filter((run) => run.agent_id !== agentId), ...runData]);
- } catch {
- toast.error(t("errors.failedToLoad"));
- } finally {
- if (!options?.silent) setRelatedLoading(false);
- }
- }, [t]);
- const loadDirectorySummaries = React.useCallback(async (agentIds: string[]) => {
- if (!agentIds.length) return;
- setDirectoryLoading(true);
- try {
- const [configResults, runResults] = await Promise.all([
- Promise.allSettled(agentIds.map((agentId) => listAgentConfigs(agentId))),
- Promise.allSettled(agentIds.map((agentId) => listAgentRuns(agentId))),
- ]);
- const nextConfigs = configResults.flatMap((result) => result.status === "fulfilled" ? result.value : []);
- const nextRuns = runResults.flatMap((result) => result.status === "fulfilled" ? result.value : []);
- setConfigs((current) => [
- ...current.filter((config) => !agentIds.includes(config.agent_id)),
- ...nextConfigs,
- ]);
- setRuns((current) => [
- ...current.filter((run) => !agentIds.includes(run.agent_id)),
- ...nextRuns,
- ]);
- } finally {
- setDirectoryLoading(false);
- }
- }, []);
- React.useEffect(() => { void loadSkills(); }, [loadSkills]);
- React.useEffect(() => { void loadDirectorySummaries(agentList.map((agent) => agent.id)); }, [agentList, loadDirectorySummaries]);
- React.useEffect(() => { void loadRelated(selectedAgent?.id); }, [loadRelated, selectedAgent?.id]);
- React.useEffect(() => { if (!selectedAgentId && agentList[0]) setSelectedAgentId(agentList[0].id); }, [agentList, selectedAgentId]);
- React.useEffect(() => { setDetailTab("overview"); }, [selectedAgent?.id]);
- async function handleDelete() {
- if (!selectedAgent) return;
- try {
- await deleteAgent(selectedAgent.id);
- toast.success(t("agents.agentDeleted"));
- setSelectedAgentId(undefined);
- setDeleteOpen(false);
- void agents.refetch();
- } catch {
- toast.error(t("agents.failedToDeleteAgent"));
- }
- }
- if (agents.loading) return <LoadingSpinner label={t("common.loading")} />;
- if (agents.error) return <ApiErrorState message={agents.error.message} onRetry={() => void agents.refetch()} />;
- return (
- <div className="space-y-6">
- <PageHeader
- title={t("agents.title")}
- description={t("agents.description")}
- actions={
- <>
- <Button variant="outline" onClick={() => { void agents.refetch(); void loadSkills(); void loadRelated(selectedAgent?.id); }}>
- <RefreshCw className="h-4 w-4" /> {t("common.refresh")}
- </Button>
- <CreateAgentDialog onCreated={() => void agents.refetch()} />
- </>
- }
- />
- <div className="grid gap-6 xl:grid-cols-[380px_minmax(0,1fr)]">
- <Card className="overflow-hidden">
- <CardHeader className="border-b border-border">
- <div className="flex items-start justify-between gap-3">
- <div>
- <CardTitle>{t("agents.agentDirectory")}</CardTitle>
- <CardDescription>
- {directoryLoading ? t("common.loading") : t("agents.shown", { shown: filtered.length, total: agentList.length })}
- </CardDescription>
- </div>
- <Badge className="border-primary/20 bg-primary/10 text-primary">{agentList.length}</Badge>
- </div>
- </CardHeader>
- <CardContent className="space-y-3 pt-4">
- <SearchInput className="sm:w-full" value={search} onChange={setSearch} placeholder={t("agents.searchPlaceholder")} />
- {filtered.length ? (
- <div className="space-y-2">
- {filtered.map((agent) => (
- <AgentDirectoryRow
- key={agent.id}
- active={agent.id === selectedAgent?.id}
- agent={agent}
- config={configByAgent.get(agent.id)}
- runs={runsByAgent.get(agent.id) ?? []}
- onClick={() => setSelectedAgentId(agent.id)}
- />
- ))}
- </div>
- ) : (
- <EmptyState icon={Search} title={t("agents.noMatchingAgents")} description={t("agents.adjustFiltersAgent")} />
- )}
- </CardContent>
- </Card>
- <Card className="overflow-hidden">
- <CardHeader className="border-b border-border">
- <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
- <div className="min-w-0">
- <div className="flex flex-wrap items-center gap-2">
- <Bot className="h-5 w-5 text-primary" />
- <CardTitle className="truncate">{selectedAgent ? demoText(selectedAgent.name, t) : t("agents.agentDetails")}</CardTitle>
- </div>
- <CardDescription className="mt-2">
- {selectedAgent ? t("agents.manageDescription") : t("agents.selectAgent")}
- </CardDescription>
- </div>
- {selectedAgent && (
- <div className="flex shrink-0 items-center gap-2">
- <Button variant="outline" size="sm" onClick={() => setEditOpen(true)}>
- {t("common.edit")}
- </Button>
- <Button variant="ghost" size="sm" onClick={() => setDeleteOpen(true)}>
- <Trash2 className="h-3 w-3 text-destructive" />
- </Button>
- </div>
- )}
- </div>
- </CardHeader>
- <CardContent className="min-w-0 pt-4">
- {selectedAgent ? (
- <div className="min-w-0 space-y-4">
- <div className="grid rounded-lg border border-border bg-muted/25 p-1 sm:w-fit sm:grid-cols-2">
- <button
- type="button"
- onClick={() => setDetailTab("overview")}
- className={[
- "rounded-md px-4 py-2 text-sm font-medium transition",
- detailTab === "overview" ? "bg-surface text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground",
- ].join(" ")}
- >
- {t("agents.overview")}
- </button>
- <button
- type="button"
- onClick={() => setDetailTab("test")}
- className={[
- "rounded-md px-4 py-2 text-sm font-medium transition",
- detailTab === "test" ? "bg-surface text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground",
- ].join(" ")}
- >
- {t("agents.test")}
- </button>
- </div>
- {detailTab === "overview" ? (
- <AgentOverview
- agent={selectedAgent}
- activeConfig={activeConfig}
- skills={skills}
- runCount={selectedRuns.length}
- failedRunCount={selectedRuns.filter((r) => r.status === "failed").length}
- />
- ) : (
- <AgentRuns
- agentId={selectedAgent.id}
- agentConfigId={activeConfig?.id}
- runs={selectedRuns}
- loading={relatedLoading}
- onRunCompleted={() => void loadRelated(selectedAgent.id, { silent: true })}
- />
- )}
- </div>
- ) : (
- <EmptyState icon={Bot} title={t("agents.noAgents")} description={t("agents.createAgentStart")} />
- )}
- </CardContent>
- </Card>
- </div>
- {selectedAgent && (
- <EditAgentDialog
- agent={selectedAgent}
- activeConfig={activeConfig}
- open={editOpen}
- onOpenChange={setEditOpen}
- onSaved={() => { void agents.refetch(); void loadRelated(selectedAgent.id); }}
- />
- )}
- <ConfirmDialog
- open={deleteOpen}
- onOpenChange={setDeleteOpen}
- title={t("agents.deleteAgent")}
- description={t("agents.deleteConfirm", { name: demoText(selectedAgent?.name, t) })}
- onConfirm={handleDelete}
- />
- </div>
- );
- }
- function AgentDirectoryRow({
- agent,
- config,
- active,
- runs,
- onClick,
- }: {
- agent: AgentDefinition;
- config?: AgentConfig;
- active: boolean;
- runs: AgentRun[];
- onClick: () => void;
- }) {
- const { t } = useTranslation();
- const model = config ? formatAgentModel(config, t) : t("agents.noModelSelected");
- const provider = config ? formatAgentProvider(config, t) : t("agents.providerNotSet");
- const skillCount = config?.skill_refs_json.length ?? 0;
- const failedCount = runs.filter((run) => run.status === "failed").length;
- const latestRun = [...runs].sort((a, b) => new Date(b.created_time).getTime() - new Date(a.created_time).getTime())[0];
- return (
- <button
- type="button"
- onClick={onClick}
- className={[
- "grid w-full grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-md border p-3 text-left transition",
- active ? "border-primary/45 bg-primary/10" : "border-border bg-muted/30 hover:bg-muted/55",
- ].join(" ")}
- >
- <span className="min-w-0">
- <span className="block truncate text-sm font-medium">{demoText(agent.name, t)}</span>
- <span className="mt-1 block truncate text-xs text-muted-foreground">
- {model}
- {provider ? ` · ${provider}` : ""}
- </span>
- <span className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
- <span>{t("agents.runsBadge", { count: runs.length })}</span>
- <span className="text-border">|</span>
- <span>{t("agents.skillsBadge", { count: skillCount })}</span>
- {failedCount ? (
- <>
- <span className="text-border">|</span>
- <span>{t("agents.failedRuns")}: {failedCount}</span>
- </>
- ) : null}
- {latestRun ? (
- <>
- <span className="text-border">|</span>
- <span>{t("agents.latest")}: {readableAgentValue(latestRun.status, t)}</span>
- </>
- ) : null}
- </span>
- </span>
- </button>
- );
- }
- function formatAgentModel(config: AgentConfig, t: ReturnType<typeof useTranslation>["t"]) {
- const value = config.model_config_json.model;
- return typeof value === "string" && value ? value : t("agents.noModelSelected");
- }
- function formatAgentProvider(config: AgentConfig, t: ReturnType<typeof useTranslation>["t"]) {
- const value = config.model_config_json.provider;
- return typeof value === "string" && value ? readableAgentValue(value, t) : t("agents.providerNotSet");
- }
- function readableAgentValue(value: string, t: ReturnType<typeof useTranslation>["t"]) {
- const fallback = value.split(/[_-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
- return t(`agents.valueLabels.${value}`, fallback);
- }
- function EditAgentDialog({
- agent,
- activeConfig,
- open,
- onOpenChange,
- onSaved,
- }: {
- agent: AgentDefinition;
- activeConfig?: AgentConfig;
- open: boolean;
- onOpenChange: (open: boolean) => void;
- onSaved: () => void;
- }) {
- const { t } = useTranslation();
- const [name, setName] = React.useState(agent.name);
- const [systemPrompt, setSystemPrompt] = React.useState(activeConfig?.system_prompt ?? "");
- const [models, setModels] = React.useState<ModelDefinition[]>([]);
- const [selectedModelId, setSelectedModelId] = React.useState("");
- const [availableSkills, setAvailableSkills] = React.useState<SkillDefinition[]>([]);
- const [selectedSkillIds, setSelectedSkillIds] = React.useState<string[]>([]);
- const [skillsLoading, setSkillsLoading] = React.useState(false);
- const [skillsError, setSkillsError] = React.useState<string | null>(null);
- const [memoryScope, setMemoryScope] = React.useState("session");
- const [temperature, setTemperature] = React.useState("0.7");
- const [maxTokens, setMaxTokens] = React.useState("4096");
- const [timeoutSeconds, setTimeoutSeconds] = React.useState("60");
- const [retryAttempts, setRetryAttempts] = React.useState("2");
- const [retryBackoffMs, setRetryBackoffMs] = React.useState("800");
- const [toolCallLimit, setToolCallLimit] = React.useState("8");
- const [contextWindow, setContextWindow] = React.useState("");
- const [outputFormat, setOutputFormat] = React.useState("text");
- const [humanApprovalPolicy, setHumanApprovalPolicy] = React.useState("never");
- const [submitting, setSubmitting] = React.useState(false);
- const currentModel = models.find((model) => model.id === selectedModelId);
- const modelOptions = React.useMemo(() => {
- return models
- .filter((model) => {
- const capabilities = model.capabilities_json ?? [];
- return capabilities.length === 0 || capabilities.includes("chat") || capabilities.includes("reasoning");
- })
- .map((model) => ({
- value: model.id,
- label: `${model.name} - ${model.model_name}`,
- }));
- }, [models]);
- React.useEffect(() => {
- if (!open) return;
- setName(agent.name);
- hydrateFromConfig(activeConfig);
- void listModels().then((items) => {
- setModels(items);
- hydrateModelSelection(items, activeConfig);
- });
- setSkillsLoading(true);
- setSkillsError(null);
- void listSkills()
- .then((skills) => setAvailableSkills(Array.isArray(skills) ? skills : []))
- .catch((err) => {
- setAvailableSkills([]);
- setSkillsError(translateApiError(err));
- })
- .finally(() => setSkillsLoading(false));
- }, [open, agent, activeConfig]);
- function hydrateFromConfig(config?: AgentConfig) {
- const modelConfig = config?.model_config_json ?? {};
- const memoryPolicy = config?.memory_policy_json ?? {};
- const runtimePolicy = config?.runtime_policy_json ?? {};
- const retryPolicy = getRecord(runtimePolicy, "retry_policy") ?? {};
- setSystemPrompt(config?.system_prompt ?? "");
- setSelectedSkillIds((config?.skill_refs_json ?? []).flatMap((item) => {
- const skillId = getString(item, "skill_id");
- return skillId ? [skillId] : [];
- }));
- setMemoryScope(getString(memoryPolicy, "memory_scope") ?? "session");
- setTemperature(getConfigString(modelConfig, "temperature", "0.7"));
- setMaxTokens(getConfigString(modelConfig, "max_tokens", "4096"));
- setTimeoutSeconds(getConfigString(modelConfig, "timeout_seconds", "60"));
- setContextWindow(getConfigString(modelConfig, "context_window", ""));
- setOutputFormat(getString(modelConfig, "output_format") ?? "text");
- setRetryAttempts(getConfigString(retryPolicy, "max_attempts", "2"));
- setRetryBackoffMs(getConfigString(retryPolicy, "backoff_ms", "800"));
- setToolCallLimit(getConfigString(runtimePolicy, "tool_call_limit", "8"));
- setHumanApprovalPolicy(getString(runtimePolicy, "human_approval_policy") ?? "never");
- }
- function hydrateModelSelection(items: ModelDefinition[], config?: AgentConfig) {
- const modelConfig = config?.model_config_json ?? {};
- const modelDefinitionId = getString(modelConfig, "model_id");
- const providerType = getString(modelConfig, "provider");
- const modelName = getString(modelConfig, "model");
- const matchedModel = items.find((model) =>
- model.id === modelDefinitionId || (model.provider_type === providerType && model.model_name === modelName)
- );
- if (matchedModel) {
- setSelectedModelId(matchedModel.id);
- return;
- }
- const firstModel = items.find((model) => {
- const capabilities = model.capabilities_json ?? [];
- return capabilities.length === 0 || capabilities.includes("chat") || capabilities.includes("reasoning");
- });
- setSelectedModelId(firstModel?.id ?? "");
- }
- function toggleSkill(skillId: string) {
- setSelectedSkillIds((current) =>
- current.includes(skillId) ? current.filter((id) => id !== skillId) : [...current, skillId]
- );
- }
- async function submit(event: React.FormEvent) {
- event.preventDefault();
- setSubmitting(true);
- try {
- await updateAgent(agent.id, { name: name.trim() });
- await createAgentConfig({
- agent_id: agent.id,
- role: "assistant",
- system_prompt: systemPrompt,
- model_config_json: {
- model_id: currentModel?.id ?? null,
- provider: currentModel?.provider_type ?? getString(activeConfig?.model_config_json ?? {}, "provider") ?? "openai",
- model: currentModel?.model_name ?? getString(activeConfig?.model_config_json ?? {}, "model") ?? "",
- temperature: parseOptionalFloat(temperature) ?? 0.7,
- max_tokens: parseOptionalInteger(maxTokens) ?? 4096,
- timeout_seconds: parseOptionalInteger(timeoutSeconds) ?? 60,
- context_window: parseOptionalInteger(contextWindow),
- output_format: outputFormat,
- },
- memory_policy_json: {
- memory_scope: memoryScope,
- },
- runtime_policy_json: {
- retry_policy: {
- max_attempts: parseOptionalInteger(retryAttempts) ?? 2,
- backoff_ms: parseOptionalInteger(retryBackoffMs) ?? 800,
- },
- tool_call_limit: parseOptionalInteger(toolCallLimit) ?? 8,
- human_approval_policy: humanApprovalPolicy,
- },
- tool_refs_json: activeConfig?.tool_refs_json ?? [],
- skill_refs_json: selectedSkillIds.map((skillId) => ({ skill_id: skillId })),
- });
- toast.success(t("agents.agentUpdated"));
- onOpenChange(false);
- onSaved();
- } catch {
- toast.error(t("agents.failedToUpdateAgent"));
- } finally {
- setSubmitting(false);
- }
- }
- return (
- <Dialog open={open} onOpenChange={onOpenChange} title={t("agents.editAgent")} className="max-w-5xl">
- <form className="space-y-5" onSubmit={submit}>
- <div className="grid gap-5 lg:grid-cols-2">
- <section className="space-y-4">
- <div className="rounded-lg border border-border bg-muted/15 p-4">
- <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">{t("agents.basicAgent")}</p>
- <div className="mt-4 space-y-4">
- <Field label={t("common.name")} required>
- <Input required value={name} onChange={(event) => setName(event.target.value)} placeholder={t("agents.namePlaceholder")} />
- </Field>
- <Field label={t("agents.systemPrompt")}>
- <Textarea value={systemPrompt} onChange={(event) => setSystemPrompt(event.target.value)} placeholder={t("agents.systemPromptPlaceholder")} rows={8} />
- </Field>
- </div>
- </div>
- <div className="rounded-lg border border-border bg-muted/10 p-4">
- <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">{t("agents.skills")}</p>
- <div className="mt-4">
- {skillsLoading ? (
- <div className="rounded-md border border-dashed border-border bg-muted/20 p-3 text-sm text-muted-foreground">
- {t("agents.loadingSkills")}
- </div>
- ) : skillsError ? (
- <div className="rounded-md border border-dashed border-red-500/30 bg-red-500/5 p-3 text-sm text-red-500">
- {skillsError}
- </div>
- ) : availableSkills.length > 0 ? (
- <div className="flex max-h-44 flex-wrap gap-2 overflow-auto pr-1">
- {availableSkills.map((skill) => (
- <button
- key={skill.id}
- type="button"
- onClick={() => toggleSkill(skill.id)}
- className={`flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm transition ${
- selectedSkillIds.includes(skill.id)
- ? "border-primary bg-primary/15 text-primary"
- : "border-border bg-muted/30 text-muted-foreground hover:bg-muted/60"
- }`}
- >
- <Sparkles className="h-3 w-3" />
- {skill.name}
- </button>
- ))}
- </div>
- ) : (
- <div className="rounded-md border border-dashed border-border bg-muted/20 p-3 text-sm text-muted-foreground">
- {t("agents.noSkillsYet")}
- </div>
- )}
- </div>
- </div>
- </section>
- <aside className="space-y-4 rounded-lg border border-border bg-surface p-4 lg:sticky lg:top-24 lg:max-h-[calc(100dvh-12rem)] lg:overflow-auto">
- <div>
- <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">{t("agents.modelAndMemory")}</p>
- <p className="mt-1 text-sm text-muted-foreground">{t("agents.modelAndMemoryHint")}</p>
- </div>
- <div className="space-y-4">
- <Field label={t("agents.model")}>
- <Select value={selectedModelId} onChange={(event) => setSelectedModelId(event.target.value)} options={modelOptions} />
- </Field>
- <div className="grid gap-3 sm:grid-cols-2">
- <Field label={t("agents.temperature")}>
- <Input value={temperature} onChange={(event) => setTemperature(event.target.value)} inputMode="decimal" />
- </Field>
- <Field label={t("agents.maxTokens")}>
- <Input value={maxTokens} onChange={(event) => setMaxTokens(event.target.value)} inputMode="numeric" />
- </Field>
- <Field label={t("agents.timeoutSeconds")}>
- <Input value={timeoutSeconds} onChange={(event) => setTimeoutSeconds(event.target.value)} inputMode="numeric" />
- </Field>
- <Field label={t("agents.contextWindow")}>
- <Input value={contextWindow} onChange={(event) => setContextWindow(event.target.value)} inputMode="numeric" placeholder={t("agents.auto")} />
- </Field>
- </div>
- </div>
- <div className="h-px bg-border" />
- <div className="space-y-4">
- <Field label={t("agents.memoryScope")}>
- <Select
- value={memoryScope}
- onChange={(event) => setMemoryScope(event.target.value)}
- options={[
- { value: "session", label: t("agents.memoryScopeSession") },
- { value: "persistent", label: t("agents.memoryScopePersistent") },
- ]}
- />
- </Field>
- </div>
- </aside>
- </div>
- <div className="rounded-lg border border-border bg-muted/10 p-4">
- <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">{t("agents.executionPolicy")}</p>
- <div className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
- <Field label={t("agents.retryAttempts")}>
- <Input value={retryAttempts} onChange={(event) => setRetryAttempts(event.target.value)} inputMode="numeric" />
- </Field>
- <Field label={t("agents.retryBackoffMs")}>
- <Input value={retryBackoffMs} onChange={(event) => setRetryBackoffMs(event.target.value)} inputMode="numeric" />
- </Field>
- <Field label={t("agents.toolCallLimit")}>
- <Input value={toolCallLimit} onChange={(event) => setToolCallLimit(event.target.value)} inputMode="numeric" />
- </Field>
- <Field label={t("agents.outputFormat")}>
- <Select
- value={outputFormat}
- onChange={(event) => setOutputFormat(event.target.value)}
- options={[
- { value: "text", label: t("agents.outputText") },
- { value: "json", label: t("common.format.json") },
- { value: "markdown", label: t("common.format.markdown") },
- ]}
- />
- </Field>
- <Field label={t("agents.humanApproval")}>
- <Select
- value={humanApprovalPolicy}
- onChange={(event) => setHumanApprovalPolicy(event.target.value)}
- options={[
- { value: "never", label: t("agents.approvalNever") },
- { value: "sensitive_actions", label: t("agents.approvalSensitiveActions") },
- { value: "before_final", label: t("agents.approvalBeforeFinal") },
- { value: "always", label: t("agents.approvalAlways") },
- ]}
- />
- </Field>
- </div>
- </div>
- <div className="flex justify-end gap-2 border-t border-border pt-4">
- <Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
- {t("common.cancel")}
- </Button>
- <Button disabled={submitting || !name.trim()}>{submitting ? t("common.saving") : t("common.save")}</Button>
- </div>
- </form>
- </Dialog>
- );
- }
- function Field({ label, children, required }: { label: string; children: React.ReactNode; required?: boolean }) {
- return (
- <label className="block space-y-2 text-sm">
- <span className="text-muted-foreground">
- {label}
- {required && <span className="text-red-500 ml-0.5">*</span>}
- </span>
- {children}
- </label>
- );
- }
- function parseOptionalInteger(value: string) {
- if (!value.trim()) return null;
- const parsed = Number.parseInt(value, 10);
- return Number.isFinite(parsed) ? parsed : null;
- }
- function parseOptionalFloat(value: string) {
- if (!value.trim()) return null;
- const parsed = Number.parseFloat(value);
- return Number.isFinite(parsed) ? parsed : null;
- }
- function getConfigString(value: JSONObject, key: string, fallback: string) {
- const item = value[key];
- if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") return String(item);
- return fallback;
- }
- function getString(value: JSONObject, key: string) {
- const item = value[key];
- return typeof item === "string" ? item : undefined;
- }
- function getRecord(value: JSONObject, key: string): JSONObject | undefined {
- const item = value[key];
- return item && typeof item === "object" && !Array.isArray(item) ? item as JSONObject : undefined;
- }
|