because I got bored of customising my CV for every job
1import { Button, TextInput, useToast } from "@cv/ui";
2import { useQueryClient } from "@tanstack/react-query";
3import { useState } from "react";
4import { useUpdateAiProviderMutation } from "@/generated/graphql";
5import { extractGraphQLErrorMessage } from "@/utils/graphql-error";
6
7interface Provider {
8 id: string;
9 label: string;
10 providerType: string;
11 model?: string | null | undefined;
12 baseUrl?: string | null | undefined;
13}
14
15interface Props {
16 providerId: string;
17 providers: Provider[];
18 onClose: () => void;
19}
20
21export const EditProviderDialog = ({
22 providerId,
23 providers,
24 onClose,
25}: Props) => {
26 const provider = providers.find((p) => p.id === providerId);
27 const { showSuccess, showError } = useToast();
28 const queryClient = useQueryClient();
29 const mutation = useUpdateAiProviderMutation();
30
31 const [form, setForm] = useState({
32 label: provider?.label ?? "",
33 model: provider?.model ?? "",
34 baseUrl: provider?.baseUrl ?? "",
35 });
36
37 if (!provider) return null;
38
39 const handleSubmit = async (e: React.FormEvent) => {
40 e.preventDefault();
41
42 try {
43 await mutation.mutateAsync({
44 providerId,
45 ...(form.label.trim() !== provider.label && {
46 label: form.label.trim(),
47 }),
48 ...(form.model.trim() !== (provider.model ?? "") && {
49 model: form.model.trim() || null,
50 }),
51 ...(form.baseUrl.trim() !== (provider.baseUrl ?? "") && {
52 baseUrl: form.baseUrl.trim() || null,
53 }),
54 });
55 await queryClient.invalidateQueries({ queryKey: ["MyAiProviders"] });
56 showSuccess("Provider updated");
57 onClose();
58 } catch (err) {
59 showError(extractGraphQLErrorMessage(err) || "Failed to update provider");
60 }
61 };
62
63 return (
64 <div className="fixed inset-0 z-50 flex items-center justify-center bg-ctp-crust/70">
65 <div className="w-full max-w-md rounded-lg bg-ctp-base p-6 shadow-lg border border-ctp-surface1">
66 <h3 className="text-lg font-medium text-ctp-text mb-4">
67 Edit Provider
68 </h3>
69 <form onSubmit={handleSubmit} className="space-y-4">
70 <TextInput
71 label="Label"
72 value={form.label}
73 onChange={(value: string) =>
74 setForm((f) => ({ ...f, label: value }))
75 }
76 />
77
78 <TextInput
79 label="Model (optional)"
80 value={form.model}
81 onChange={(value: string) =>
82 setForm((f) => ({ ...f, model: value }))
83 }
84 placeholder={
85 provider.providerType === "anthropic"
86 ? "claude-sonnet-4-5-20250929"
87 : "gpt-4o-mini"
88 }
89 />
90
91 <TextInput
92 label="Base URL (optional)"
93 value={form.baseUrl}
94 onChange={(value: string) =>
95 setForm((f) => ({ ...f, baseUrl: value }))
96 }
97 placeholder={
98 provider.providerType === "anthropic"
99 ? "https://api.anthropic.com"
100 : "https://api.openai.com"
101 }
102 />
103
104 <div className="flex gap-3">
105 <Button type="submit" disabled={mutation.isPending}>
106 {mutation.isPending ? "Saving..." : "Save Changes"}
107 </Button>
108 <Button type="button" variant="ghost" onClick={onClose}>
109 Cancel
110 </Button>
111 </div>
112 </form>
113 </div>
114 </div>
115 );
116};