From ea9301c7716627b13cecd01e7799362b7f597784 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Mon, 12 May 2025 16:37:39 -0700 Subject: [PATCH] Support delete custom model (#136) --- package-lock.json | 283 +++++++++++++++++++- package.json | 3 +- src/components/settings/ModelsSection.tsx | 97 ++++++- src/components/ui/alert-dialog.tsx | 155 +++++++++++ src/hooks/useDeleteCustomModel.ts | 49 ++++ src/ipc/handlers/language_model_handlers.ts | 77 +++++- src/ipc/ipc_client.ts | 17 ++ src/preload.ts | 2 + 8 files changed, 664 insertions(+), 19 deletions(-) create mode 100644 src/components/ui/alert-dialog.tsx create mode 100644 src/hooks/useDeleteCustomModel.ts diff --git a/package-lock.json b/package-lock.json index cb56939..db5997a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,13 +18,14 @@ "@monaco-editor/react": "^4.7.0-rc.0", "@openrouter/ai-sdk-provider": "^0.4.5", "@radix-ui/react-accordion": "^1.2.4", + "@radix-ui/react-alert-dialog": "^1.1.13", "@radix-ui/react-dialog": "^1.1.7", "@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-label": "^2.1.4", "@radix-ui/react-popover": "^1.1.7", "@radix-ui/react-select": "^2.2.2", "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-switch": "^1.2.0", "@radix-ui/react-toggle": "^1.1.3", "@radix-ui/react-toggle-group": "^1.1.3", @@ -3774,6 +3775,57 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.13.tgz", + "integrity": "sha512-/uPs78OwxGxslYOG5TKeUsv9fZC0vo376cXSADdKirTmsLJU2au6L3n34c3p6W26rFDDDze/hwy4fYeNd0qdGA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.13", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-slot": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz", + "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.4.tgz", @@ -3853,6 +3905,24 @@ } } }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -3884,22 +3954,22 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.11.tgz", - "integrity": "sha512-yI7S1ipkP5/+99qhSI6nthfo/tR6bL6Zgxi/+1UO6qPa6UeM6nlafWcQ65vB4rU2XjgjMfMhI3k9Y5MztA62VQ==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.13.tgz", + "integrity": "sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-dismissable-layer": "1.1.9", "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.6", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-portal": "1.1.8", "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-slot": "1.2.2", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" @@ -3919,6 +3989,105 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz", + "integrity": "sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.6.tgz", + "integrity": "sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.8.tgz", + "integrity": "sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz", + "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", @@ -4111,6 +4280,24 @@ } } }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.11.tgz", @@ -4148,6 +4335,24 @@ } } }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.4.tgz", @@ -4251,6 +4456,24 @@ } } }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.7.tgz", @@ -4325,6 +4548,24 @@ } } }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-separator": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.4.tgz", @@ -4349,9 +4590,9 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", - "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -4483,6 +4724,24 @@ } } }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", diff --git a/package.json b/package.json index bc47878..6f87012 100644 --- a/package.json +++ b/package.json @@ -81,13 +81,14 @@ "@monaco-editor/react": "^4.7.0-rc.0", "@openrouter/ai-sdk-provider": "^0.4.5", "@radix-ui/react-accordion": "^1.2.4", + "@radix-ui/react-alert-dialog": "^1.1.13", "@radix-ui/react-dialog": "^1.1.7", "@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-label": "^2.1.4", "@radix-ui/react-popover": "^1.1.7", "@radix-ui/react-select": "^2.2.2", "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-switch": "^1.2.0", "@radix-ui/react-toggle": "^1.1.3", "@radix-ui/react-toggle-group": "^1.1.3", diff --git a/src/components/settings/ModelsSection.tsx b/src/components/settings/ModelsSection.tsx index 1bc43da..afc1d5e 100644 --- a/src/components/settings/ModelsSection.tsx +++ b/src/components/settings/ModelsSection.tsx @@ -1,10 +1,21 @@ import { useState } from "react"; -import { AlertTriangle, PlusIcon } from "lucide-react"; +import { AlertTriangle, PlusIcon, TrashIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { CreateCustomModelDialog } from "@/components/CreateCustomModelDialog"; import { useLanguageModelsForProvider } from "@/hooks/useLanguageModelsForProvider"; // Use the hook directly here +import { useDeleteCustomModel } from "@/hooks/useDeleteCustomModel"; // Import the new hook +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; interface ModelsSectionProps { providerId: string; @@ -12,6 +23,9 @@ interface ModelsSectionProps { export function ModelsSection({ providerId }: ModelsSectionProps) { const [isCustomModelDialogOpen, setIsCustomModelDialogOpen] = useState(false); + const [isConfirmDeleteDialogOpen, setIsConfirmDeleteDialogOpen] = + useState(false); + const [modelToDelete, setModelToDelete] = useState(null); // Fetch custom models within this component now const { @@ -21,6 +35,30 @@ export function ModelsSection({ providerId }: ModelsSectionProps) { refetch: refetchModels, } = useLanguageModelsForProvider(providerId); + const { mutate: deleteModel, isPending: isDeleting } = useDeleteCustomModel({ + onSuccess: () => { + refetchModels(); // Refetch models list after successful deletion + // Optionally show a success toast here + }, + onError: (error: Error) => { + // Optionally show an error toast here + console.error("Failed to delete model:", error); + }, + }); + + const handleDeleteClick = (modelApiName: string) => { + setModelToDelete(modelApiName); + setIsConfirmDeleteDialogOpen(true); + }; + + const handleConfirmDelete = () => { + if (modelToDelete) { + deleteModel({ providerId, modelApiName: modelToDelete }); + setModelToDelete(null); + } + setIsConfirmDeleteDialogOpen(false); + }; + return (

Models

@@ -53,7 +91,17 @@ export function ModelsSection({ providerId }: ModelsSectionProps) {

{model.displayName}

- {/* Optional: Add an edit/delete button here later */} + {model.type === "custom" && ( + + )}

{model.apiName} @@ -75,11 +123,17 @@ export function ModelsSection({ providerId }: ModelsSectionProps) { )} - {model.tag && ( +

- {model.tag} + {model.type === "cloud" ? "Built-in" : "Custom"} - )} + + {model.tag && ( + + {model.tag} + + )} +
))} @@ -109,6 +163,39 @@ export function ModelsSection({ providerId }: ModelsSectionProps) { }} providerId={providerId} /> + + + + + + Are you sure you want to delete this model? + + + This action cannot be undone. This will permanently delete the + custom model " + {modelToDelete + ? models?.find((m) => m.apiName === modelToDelete) + ?.displayName || modelToDelete + : ""} + " (API Name: {modelToDelete}). + + + + setModelToDelete(null)}> + Cancel + + + {isDeleting ? "Deleting..." : "Yes, delete it"} + + + + ); } diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..5284517 --- /dev/null +++ b/src/components/ui/alert-dialog.tsx @@ -0,0 +1,155 @@ +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; + +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return ; +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ); +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/src/hooks/useDeleteCustomModel.ts b/src/hooks/useDeleteCustomModel.ts new file mode 100644 index 0000000..eb029f8 --- /dev/null +++ b/src/hooks/useDeleteCustomModel.ts @@ -0,0 +1,49 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { IpcClient } from "@/ipc/ipc_client"; + +interface DeleteCustomModelParams { + providerId: string; + modelApiName: string; +} + +export function useDeleteCustomModel({ + onSuccess, + onError, +}: { + onSuccess?: () => void; + onError?: (error: Error) => void; +}) { + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: async (params: DeleteCustomModelParams) => { + if (!params.providerId || !params.modelApiName) { + throw new Error( + "Provider ID and Model API Name are required for deletion.", + ); + } + const ipcClient = IpcClient.getInstance(); + // This method will be added to IpcClient next + await ipcClient.deleteCustomModel(params); + }, + onSuccess: (data, params: DeleteCustomModelParams) => { + // Invalidate queries related to language models for the specific provider + queryClient.invalidateQueries({ + queryKey: ["language-models", params.providerId], + }); + // Invalidate general model list if needed + queryClient.invalidateQueries({ queryKey: ["languageModels"] }); + onSuccess?.(); + }, + onError: (error: Error) => { + console.error("Error deleting custom model:", error); + onError?.(error); + }, + meta: { + // Optional: for global error handling like toasts + showErrorToast: true, + }, + }); + + return mutation; +} diff --git a/src/ipc/handlers/language_model_handlers.ts b/src/ipc/handlers/language_model_handlers.ts index d81dee2..8617e49 100644 --- a/src/ipc/handlers/language_model_handlers.ts +++ b/src/ipc/handlers/language_model_handlers.ts @@ -12,10 +12,11 @@ import { } from "../shared/language_model_helpers"; import { db } from "@/db"; import { + language_models, language_model_providers as languageModelProvidersSchema, language_models as languageModelsSchema, } from "@/db/schema"; -import { eq } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import { IpcMainInvokeEvent } from "electron"; const logger = log.scope("language_model_handlers"); @@ -129,6 +130,80 @@ export function registerLanguageModelHandlers() { }, ); + handle( + "delete-custom-language-model", + async ( + event: IpcMainInvokeEvent, + params: { modelId: string }, + ): Promise => { + const { modelId: apiName } = params; + + // Validation + if (!apiName) { + throw new Error("Model API name (modelId) is required"); + } + + logger.info( + `Handling delete-custom-language-model for apiName: ${apiName}`, + ); + + const existingModel = await db + .select() + .from(languageModelsSchema) + .where(eq(languageModelsSchema.apiName, apiName)) + .get(); + + if (!existingModel) { + throw new Error( + `A model with API name (modelId) "${apiName}" was not found`, + ); + } + + await db + .delete(languageModelsSchema) + .where(eq(languageModelsSchema.apiName, apiName)); + }, + ); + + handle( + "delete-custom-model", + async ( + _event: IpcMainInvokeEvent, + params: { providerId: string; modelApiName: string }, + ): Promise => { + const { providerId, modelApiName } = params; + logger.info( + `Handling delete-custom-model for ${providerId} / ${modelApiName}`, + ); + if (!providerId || !modelApiName) { + throw new Error("Provider ID and Model API Name are required."); + } + logger.info( + `Attempting to delete custom model ${modelApiName} for provider ${providerId}`, + ); + + const result = db + .delete(language_models) + .where( + and( + eq(language_models.provider_id, providerId), + eq(language_models.apiName, modelApiName), + ), + ) + .run(); + + if (result.changes === 0) { + logger.warn( + `No custom model found matching providerId=${providerId} and apiName=${modelApiName} for deletion.`, + ); + } else { + logger.info( + `Successfully deleted ${result.changes} custom model(s) with apiName=${modelApiName} for provider=${providerId}`, + ); + } + }, + ); + handle( "get-language-models", async ( diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index f8a0f87..7675575 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -58,6 +58,11 @@ export interface DeepLinkData { url?: string; } +interface DeleteCustomModelParams { + providerId: string; + modelApiName: string; +} + export class IpcClient { private static instance: IpcClient; private ipcRenderer: IpcRenderer; @@ -761,5 +766,17 @@ export class IpcClient { await this.ipcRenderer.invoke("create-custom-language-model", params); } + public async deleteCustomLanguageModel(modelId: string): Promise { + return this.ipcRenderer.invoke("delete-custom-language-model", modelId); + } + + async deleteCustomModel(params: DeleteCustomModelParams): Promise { + return this.ipcRenderer.invoke("delete-custom-model", params); + } + // --- End window control methods --- + + // --- Language Model Operations --- + + // --- App Operations --- } diff --git a/src/preload.ts b/src/preload.ts index b22ad26..75a6b71 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -9,6 +9,8 @@ const validInvokeChannels = [ "create-custom-language-model", "get-language-model-providers", "create-custom-language-model-provider", + "delete-custom-language-model", + "delete-custom-model", "chat:add-dep", "chat:message", "chat:cancel",