Files
kandev/apps/web/components/settings/dynamic-agent-profile-editor.tsx
2026-09-10 11:35:46 +01:00

172 lines
5.9 KiB
TypeScript

"use client";
import { useTranslation } from "react-i18next";
import { IconAlertTriangle, IconInfoCircle } from "@tabler/icons-react";
import { Alert, AlertDescription, AlertTitle } from "@kandev/ui/alert";
import { Button } from "@kandev/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@kandev/ui/card";
import { Separator } from "@kandev/ui/separator";
import { Switch } from "@kandev/ui/switch";
import { AgentLogo } from "@/components/agent-logo";
import { DynamicAgentCandidateList } from "@/components/settings/dynamic-agent-candidate-list";
import { ProfileEnabledHelp } from "@/components/settings/profile-enabled-help";
import { ProfileNameField } from "@/components/settings/profile-form-fields";
import { useDynamicAgentProfileEditorState } from "@/components/settings/dynamic-agent-profile-editor-state";
import type { Agent, AgentProfile } from "@/lib/types/http";
type DynamicAgentProfileEditorProps = {
agent: Agent;
profile: AgentProfile;
isCreating?: boolean;
/** When supplied, render as a draft editor owned by the parent save flow. */
onDraftChange?: (patch: Pick<AgentProfile, "name" | "dynamic" | "enabled">) => void;
};
function DynamicRoutingPolicyHelp() {
const { t } = useTranslation();
return (
<div
className="flex gap-3 rounded-md border bg-muted/30 p-3"
data-testid="dynamic-routing-policy-help"
>
<IconInfoCircle className="mt-0.5 size-4 shrink-0 text-muted-foreground" aria-hidden />
<div className="min-w-0 space-y-1 text-xs leading-relaxed text-muted-foreground">
<p className="font-medium text-foreground">{t("agents:dynamicRoutingPolicyTitle")}</p>
<p>{t("agents:dynamicRoutingPolicySwitchable")}</p>
<p>{t("agents:dynamicRoutingPolicyExcluded")}</p>
<p>{t("agents:dynamicRoutingPolicyRecovery")}</p>
<p>{t("agents:dynamicPolicyWaitExplanation")}</p>
</div>
</div>
);
}
function DynamicProfileEditorHeader({
agent,
profile,
name,
enabled,
onEnabledChange,
}: {
agent: Agent;
profile: AgentProfile;
name: string;
enabled: boolean;
onEnabledChange: (enabled: boolean) => void;
}) {
const { t } = useTranslation();
return (
<>
<div className="flex flex-col items-stretch justify-between gap-4 sm:flex-row sm:items-start">
<div className="min-w-0">
<h2 className="flex min-w-0 items-center gap-2 text-2xl font-bold wrap-break-word">
<AgentLogo agentName={agent.name} size={28} className="shrink-0" />
{profile.agentDisplayName} • {name}
</h2>
<p className="mt-1 text-sm text-muted-foreground">
{t("agents:dynamicProfileDescription")}
</p>
</div>
<div className="flex items-center gap-3 sm:shrink-0">
<div className="flex items-center gap-1 text-left sm:text-right">
<p className="text-sm font-medium">{t("agents:enabled")}</p>
<ProfileEnabledHelp />
</div>
<Switch
checked={enabled}
onCheckedChange={onEnabledChange}
data-testid="dynamic-profile-enabled-toggle"
aria-label={enabled ? t("agents:disableProfile") : t("agents:enableProfile")}
/>
</div>
</div>
<Separator />
</>
);
}
export function DynamicAgentProfileEditor({
agent,
profile,
isCreating = false,
onDraftChange,
}: DynamicAgentProfileEditorProps) {
const { t } = useTranslation();
const state = useDynamicAgentProfileEditorState({ agent, profile, onDraftChange });
if (!state.routingEnabled) {
return (
<Card>
<CardContent className="py-12 text-center">
<p className="text-sm text-muted-foreground">{t("agents:dynamicProfileDisabled")}</p>
</CardContent>
</Card>
);
}
const editorContent = (
<>
<ProfileNameField
id="dynamic-profile-name"
testId="dynamic-profile-name"
value={state.name}
onChange={state.updateName}
/>
<DynamicRoutingPolicyHelp />
<DynamicAgentCandidateList
candidates={state.candidates}
concreteProfiles={state.concreteProfiles}
availableProfileOptions={state.availableProfileOptions}
enabledLabel={state.enabledLabel}
addCandidate={state.addCandidate}
moveCandidate={state.moveCandidate}
removeCandidate={state.removeCandidate}
updateCandidate={state.updateCandidate}
updateCandidatePolicy={state.updateCandidatePolicy}
/>
</>
);
return (
<div className="min-w-0 space-y-6 overflow-x-hidden">
{state.standalone && !isCreating && (
<DynamicProfileEditorHeader
agent={agent}
profile={profile}
name={state.name}
enabled={state.profileEnabled}
onEnabledChange={state.updateProfileEnabled}
/>
)}
{state.hasExternalConflict ? (
<Alert variant="destructive" data-testid="dynamic-profile-external-change-alert">
<IconAlertTriangle className="h-4 w-4" />
<AlertTitle>{t("agents:profileExternalChangeTitle")}</AlertTitle>
<AlertDescription className="flex flex-col items-start gap-3 sm:flex-row sm:items-center sm:justify-between">
<span>{t("agents:profileExternalChangeDescription")}</span>
<Button
type="button"
variant="outline"
className="min-h-11 shrink-0"
onClick={state.discardDraft}
data-testid="dynamic-profile-external-change-discard"
>
{t("agents:profileExternalChangeDiscard")}
</Button>
</AlertDescription>
</Alert>
) : null}
{state.standalone ? (
<Card>
<CardHeader>
<CardTitle>{t("agents:dynamicProfileSettings")}</CardTitle>
</CardHeader>
<CardContent className="space-y-5">{editorContent}</CardContent>
</Card>
) : (
<div className="space-y-5">{editorContent}</div>
)}
</div>
);
}