141 lines
4.5 KiB
TypeScript
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);
|
|
}
|