Files
kandev/apps/web/components/settings/profile-capability-helpers.tsx
T
2026-09-25 10:31:58 +01:00

141 lines
4.5 KiB
TypeScript

import { Trans, useTranslation } from "react-i18next";
import { useEffect } from "react";
import { IconTerminal2 } from "@tabler/icons-react";
import { Button } from "@kandev/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@kandev/ui/dialog";
import { useProfileModelCapabilities } from "@/hooks/domains/settings/use-profile-model-capabilities";
import { modelConfigOptions } from "@/components/settings/profile-model-config";
import type { CommandEntry, ModeEntry, ModelConfig } from "@/lib/types/http";
import type { ProfileFormData } from "./profile-model-fields";
type ProfileFormCapabilitySelection = {
model: string;
mode: string;
provider_kind?: string;
config_options?: Record<string, string>;
};
export function useProfileFormCapabilities(
agentName: string,
profile: ProfileFormCapabilitySelection,
modelConfig: ModelConfig,
onChange: (patch: { config_options: Record<string, string> }) => void,
onPendingChange?: (pending: boolean) => void,
) {
const result = useProfileModelCapabilities(agentName, profile, modelConfig, onChange, {
skipCapabilityProbe: profile.provider_kind === "openai_compatible",
});
useEffect(() => {
onPendingChange?.(result.isConfigResolutionPending);
}, [onPendingChange, result.isConfigResolutionPending]);
return {
...result,
configOptions: modelConfigOptions(
result.configOptions ? { ...modelConfig, config_options: result.configOptions } : modelConfig,
),
};
}
// An example slash command the user types verbatim — an identifier, not copy.
const EXAMPLE_COMMAND = "/init";
export function CommandsButton({ commands }: { commands: CommandEntry[] }) {
const { t } = useTranslation();
if (commands.length === 0) return null;
return (
<Dialog>
<DialogTrigger asChild>
<Button
type="button"
variant="outline"
size="sm"
className="cursor-pointer"
data-testid="profile-commands-button"
>
<IconTerminal2 className="mr-2 h-4 w-4" />
{t("agents:availableCommandsCount", { count: commands.length })}
</Button>
</DialogTrigger>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>{t("agents:availableSlashCommands")}</DialogTitle>
<DialogDescription>
<Trans
i18nKey="agents:availableSlashCommandsHelp"
values={{ example: EXAMPLE_COMMAND }}
>
<code />
</Trans>
</DialogDescription>
</DialogHeader>
<div className="max-h-[60vh] overflow-y-auto space-y-2">
{commands.map((command) => (
<div key={command.name} className="rounded-md border p-3">
<code className="text-sm font-semibold">/{command.name}</code>
{command.description && (
<p className="text-xs text-muted-foreground mt-1">{command.description}</p>
)}
</div>
))}
</div>
</DialogContent>
</Dialog>
);
}
export function findActiveMode(
modes: ModeEntry[],
selectedMode: string,
currentModeId?: string,
): ModeEntry | undefined {
if (modes.length === 0) return undefined;
const modeId = selectedMode || currentModeId || modes[0]?.id;
return modes.find((mode) => mode.id === modeId);
}
export function profileModelIsDirty(profile: ProfileFormData, baseline?: ProfileFormData): boolean {
if (!baseline) return false;
return (
profile.model !== baseline.model ||
JSON.stringify(profile.config_options ?? {}) !== JSON.stringify(baseline.config_options ?? {})
);
}
export function profileFallbackModelIsDirty(
profile: ProfileFormData,
baseline?: ProfileFormData,
): boolean {
return Boolean(baseline && (profile.fallback_model ?? "") !== (baseline.fallback_model ?? ""));
}
export function profileAutoFallbackIsDirty(
profile: ProfileFormData,
baseline?: ProfileFormData,
): boolean {
return Boolean(
baseline && (profile.auto_fallback ?? false) !== (baseline.auto_fallback ?? false),
);
}
export function profileRequireExactModelIsDirty(
profile: ProfileFormData,
baseline?: ProfileFormData,
): boolean {
return Boolean(
baseline && (profile.require_exact_model ?? false) !== (baseline.require_exact_model ?? false),
);
}
export function profileModeIsDirty(profile: ProfileFormData, baseline?: ProfileFormData): boolean {
return Boolean(baseline && profile.mode !== baseline.mode);
}