## Summary Adds the ability to edit existing custom AI providers through the settings UI. ## Changes Made - **UI Changes:** - Added "Edit Provider" button to custom provider dropdown menu - Modified `CreateCustomProviderDialog` to support edit mode - **Backend Changes:** - Implemented `editCustomLanguageModelProvider` handler in `language_model_handlers.ts` - Added corresponding IPC client method - Database transaction ensures atomicity when updating provider and associated models - **Testing:** - Added comprehensive e2e test covering edit functionality - Tests verify form pre-population, field updates, and UI persistence https://github.com/user-attachments/assets/e8c8600e-4fb7-4816-be95-993ede1224d4 ## Closes Fixes #1232 and #1171 <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds edit support for custom language model providers in Settings. Users can update provider ID, name, API base URL, and API key env var, with safe backend updates that also retarget associated models if the ID changes. - New Features - Added “Edit Provider” option in the custom provider menu. - Dialog supports edit mode with pre-filled fields, unified loading state, and update button text. - New IPC handler to edit providers with validation and a transaction; updates linked models when IDs change. - IPC client and preload channel updated; React hook exposes editProvider mutation with cache invalidation. - Added e2e test covering the full edit flow. <!-- End of auto-generated description by cubic. -->
214 lines
6.5 KiB
TypeScript
214 lines
6.5 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
} from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Loader2 } from "lucide-react";
|
|
import { useCustomLanguageModelProvider } from "@/hooks/useCustomLanguageModelProvider";
|
|
import type { LanguageModelProvider } from "@/ipc/ipc_types";
|
|
|
|
interface CreateCustomProviderDialogProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSuccess: () => void;
|
|
editingProvider?: LanguageModelProvider | null;
|
|
}
|
|
|
|
export function CreateCustomProviderDialog({
|
|
isOpen,
|
|
onClose,
|
|
onSuccess,
|
|
editingProvider = null,
|
|
}: CreateCustomProviderDialogProps) {
|
|
const [id, setId] = useState("");
|
|
const [name, setName] = useState("");
|
|
const [apiBaseUrl, setApiBaseUrl] = useState("");
|
|
const [envVarName, setEnvVarName] = useState("");
|
|
const [errorMessage, setErrorMessage] = useState("");
|
|
const isEditMode = Boolean(editingProvider);
|
|
|
|
const { createProvider, editProvider, isCreating, isEditing, error } =
|
|
useCustomLanguageModelProvider();
|
|
// Load provider data when editing
|
|
useEffect(() => {
|
|
if (editingProvider && isOpen) {
|
|
const cleanId = editingProvider.id?.startsWith("custom::")
|
|
? editingProvider.id.replace("custom::", "")
|
|
: editingProvider.id || "";
|
|
setId(cleanId);
|
|
setName(editingProvider.name || "");
|
|
setApiBaseUrl(editingProvider.apiBaseUrl || "");
|
|
setEnvVarName(editingProvider.envVarName || "");
|
|
} else if (!isOpen) {
|
|
// Reset form when dialog closes
|
|
setId("");
|
|
setName("");
|
|
setApiBaseUrl("");
|
|
setEnvVarName("");
|
|
setErrorMessage("");
|
|
}
|
|
}, [editingProvider, isOpen]);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setErrorMessage("");
|
|
|
|
try {
|
|
if (isEditMode && editingProvider) {
|
|
const cleanId = editingProvider.id?.startsWith("custom::")
|
|
? editingProvider.id.replace("custom::", "")
|
|
: editingProvider.id || "";
|
|
await editProvider({
|
|
id: cleanId,
|
|
name: name.trim(),
|
|
apiBaseUrl: apiBaseUrl.trim(),
|
|
envVarName: envVarName.trim() || undefined,
|
|
});
|
|
} else {
|
|
await createProvider({
|
|
id: id.trim(),
|
|
name: name.trim(),
|
|
apiBaseUrl: apiBaseUrl.trim(),
|
|
envVarName: envVarName.trim() || undefined,
|
|
});
|
|
}
|
|
|
|
// Reset form
|
|
setId("");
|
|
setName("");
|
|
setApiBaseUrl("");
|
|
setEnvVarName("");
|
|
|
|
onSuccess();
|
|
} catch (error) {
|
|
setErrorMessage(
|
|
error instanceof Error
|
|
? error.message
|
|
: `Failed to ${isEditMode ? "edit" : "create"} custom provider`,
|
|
);
|
|
}
|
|
};
|
|
|
|
const handleClose = () => {
|
|
if (!isCreating && !isEditing) {
|
|
setErrorMessage("");
|
|
onClose();
|
|
}
|
|
};
|
|
const isLoading = isCreating || isEditing;
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{isEditMode ? "Edit Custom Provider" : "Add Custom Provider"}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{isEditMode
|
|
? "Update your custom language model provider configuration."
|
|
: "Connect to a custom language model provider API."}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4 pt-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="id">Provider ID</Label>
|
|
<Input
|
|
id="id"
|
|
value={id}
|
|
onChange={(e) => setId(e.target.value)}
|
|
placeholder="E.g., my-provider"
|
|
required
|
|
disabled={isLoading || isEditMode}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
A unique identifier for this provider (no spaces).
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="name">Display Name</Label>
|
|
<Input
|
|
id="name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="E.g., My Provider"
|
|
required
|
|
disabled={isLoading}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
The name that will be displayed in the UI.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="apiBaseUrl">API Base URL</Label>
|
|
<Input
|
|
id="apiBaseUrl"
|
|
value={apiBaseUrl}
|
|
onChange={(e) => setApiBaseUrl(e.target.value)}
|
|
placeholder="E.g., https://api.example.com/v1"
|
|
required
|
|
disabled={isLoading}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
The base URL for the API endpoint.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="envVarName">Environment Variable (Optional)</Label>
|
|
<Input
|
|
id="envVarName"
|
|
value={envVarName}
|
|
onChange={(e) => setEnvVarName(e.target.value)}
|
|
placeholder="E.g., MY_PROVIDER_API_KEY"
|
|
disabled={isLoading}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Environment variable name for the API key.
|
|
</p>
|
|
</div>
|
|
|
|
{(errorMessage || error) && (
|
|
<div className="text-sm text-red-500">
|
|
{errorMessage ||
|
|
(error instanceof Error
|
|
? error.message
|
|
: "Failed to create custom provider")}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-end gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={handleClose}
|
|
disabled={isLoading}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" disabled={isLoading}>
|
|
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
{isLoading
|
|
? isEditMode
|
|
? "Updating..."
|
|
: "Adding..."
|
|
: isEditMode
|
|
? "Update Provider"
|
|
: "Add Provider"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|