blob: 09bc28df54e3d80260e1cbf6586a1f93a059d94e [file] [log] [blame]
gio69ff7592025-07-03 06:27:21 +00001import { useProjectId, useGithubService, useStateStore, useGeminiService, useAnthropicService } from "@/lib/state";
giod0026612025-05-08 13:00:36 +00002import { Form, FormControl, FormField, FormItem, FormMessage } from "./components/ui/form";
3import { Input } from "./components/ui/input";
4import { useForm } from "react-hook-form";
5import { z } from "zod";
6import { zodResolver } from "@hookform/resolvers/zod";
7import { Button } from "./components/ui/button";
8import { useToast } from "@/hooks/use-toast";
gio02f1cad2025-05-13 11:51:55 +00009import { CircleCheck, CircleX } from "lucide-react";
giod0026612025-05-08 13:00:36 +000010import { useState, useCallback } from "react";
gio7f98e772025-05-07 11:00:14 +000011
gio69148322025-06-19 23:16:12 +040012const githubSchema = z.object({
giod0026612025-05-08 13:00:36 +000013 githubToken: z.string().min(1, "GitHub token is required"),
gio7f98e772025-05-07 11:00:14 +000014});
15
gio69148322025-06-19 23:16:12 +040016const geminiSchema = z.object({
17 geminiApiKey: z.string().min(1, "Gemini API token is required"),
18});
19
gio69ff7592025-07-03 06:27:21 +000020const anthropicSchema = z.object({
21 anthropicApiKey: z.string().min(1, "Anthropic API token is required"),
22});
23
gio7f98e772025-05-07 11:00:14 +000024export function Integrations() {
giod0026612025-05-08 13:00:36 +000025 const { toast } = useToast();
26 const store = useStateStore();
27 const projectId = useProjectId();
gio69148322025-06-19 23:16:12 +040028 const [isEditingGithub, setIsEditingGithub] = useState(false);
29 const [isEditingGemini, setIsEditingGemini] = useState(false);
gio69ff7592025-07-03 06:27:21 +000030 const [isEditingAnthropic, setIsEditingAnthropic] = useState(false);
giod0026612025-05-08 13:00:36 +000031 const githubService = useGithubService();
gio69148322025-06-19 23:16:12 +040032 const geminiService = useGeminiService();
gio69ff7592025-07-03 06:27:21 +000033 const anthropicService = useAnthropicService();
giod0026612025-05-08 13:00:36 +000034 const [isSaving, setIsSaving] = useState(false);
gio7f98e772025-05-07 11:00:14 +000035
gio69148322025-06-19 23:16:12 +040036 const githubForm = useForm<z.infer<typeof githubSchema>>({
37 resolver: zodResolver(githubSchema),
giod0026612025-05-08 13:00:36 +000038 mode: "onChange",
39 });
gio7f98e772025-05-07 11:00:14 +000040
gio69148322025-06-19 23:16:12 +040041 const geminiForm = useForm<z.infer<typeof geminiSchema>>({
42 resolver: zodResolver(geminiSchema),
43 mode: "onChange",
44 });
45
gio69ff7592025-07-03 06:27:21 +000046 const anthropicForm = useForm<z.infer<typeof anthropicSchema>>({
47 resolver: zodResolver(anthropicSchema),
48 mode: "onChange",
49 });
50
gio69148322025-06-19 23:16:12 +040051 const onGithubSubmit = useCallback(
52 async (data: z.infer<typeof githubSchema>) => {
giod0026612025-05-08 13:00:36 +000053 if (!projectId) return;
gio7f98e772025-05-07 11:00:14 +000054
giod0026612025-05-08 13:00:36 +000055 setIsSaving(true);
gio7f98e772025-05-07 11:00:14 +000056
giod0026612025-05-08 13:00:36 +000057 try {
58 const response = await fetch(`/api/project/${projectId}/github-token`, {
59 method: "POST",
60 headers: {
61 "Content-Type": "application/json",
62 },
63 body: JSON.stringify({ githubToken: data.githubToken }),
64 });
gio7f98e772025-05-07 11:00:14 +000065
giod0026612025-05-08 13:00:36 +000066 if (!response.ok) {
67 throw new Error("Failed to save GitHub token");
68 }
gio7f98e772025-05-07 11:00:14 +000069
giod0026612025-05-08 13:00:36 +000070 await store.refreshEnv();
gio69148322025-06-19 23:16:12 +040071 setIsEditingGithub(false);
72 githubForm.reset();
giod0026612025-05-08 13:00:36 +000073 toast({
74 title: "GitHub token saved successfully",
75 });
76 } catch (error) {
77 toast({
78 variant: "destructive",
79 title: "Failed to save GitHub token",
80 description: error instanceof Error ? error.message : "Unknown error",
81 });
82 } finally {
83 setIsSaving(false);
84 }
85 },
gio69148322025-06-19 23:16:12 +040086 [projectId, store, githubForm, toast, setIsEditingGithub, setIsSaving],
giod0026612025-05-08 13:00:36 +000087 );
gio7f98e772025-05-07 11:00:14 +000088
gio69148322025-06-19 23:16:12 +040089 const onGeminiSubmit = useCallback(
90 async (data: z.infer<typeof geminiSchema>) => {
91 if (!projectId) return;
92 setIsSaving(true);
93 try {
94 const response = await fetch(`/api/project/${projectId}/gemini-token`, {
95 method: "POST",
96 headers: {
97 "Content-Type": "application/json",
98 },
99 body: JSON.stringify({ geminiApiKey: data.geminiApiKey }),
100 });
101 if (!response.ok) {
102 throw new Error("Failed to save Gemini token");
103 }
104 await store.refreshEnv();
105 setIsEditingGemini(false);
106 geminiForm.reset();
107 toast({
108 title: "Gemini token saved successfully",
109 });
110 } catch (error) {
111 toast({
112 variant: "destructive",
113 title: "Failed to save Gemini token",
114 description: error instanceof Error ? error.message : "Unknown error",
115 });
116 } finally {
117 setIsSaving(false);
118 }
119 },
120 [projectId, store, geminiForm, toast, setIsEditingGemini, setIsSaving],
121 );
122
gio69ff7592025-07-03 06:27:21 +0000123 const onAnthropicSubmit = useCallback(
124 async (data: z.infer<typeof anthropicSchema>) => {
125 if (!projectId) return;
126 setIsSaving(true);
127 try {
128 const response = await fetch(`/api/project/${projectId}/anthropic-token`, {
129 method: "POST",
130 headers: {
131 "Content-Type": "application/json",
132 },
133 body: JSON.stringify({ anthropicApiKey: data.anthropicApiKey }),
134 });
135 if (!response.ok) {
136 throw new Error("Failed to save Anthropic token");
137 }
138 await store.refreshEnv();
139 setIsEditingAnthropic(false);
140 anthropicForm.reset();
141 toast({
142 title: "Anthropic token saved successfully",
143 });
144 } catch (error) {
145 toast({
146 variant: "destructive",
147 title: "Failed to save Anthropic token",
148 description: error instanceof Error ? error.message : "Unknown error",
149 });
150 } finally {
151 setIsSaving(false);
152 }
153 },
154 [projectId, store, anthropicForm, toast, setIsEditingAnthropic, setIsSaving],
155 );
156
gio69148322025-06-19 23:16:12 +0400157 const handleCancelGithub = () => {
158 setIsEditingGithub(false);
159 githubForm.reset();
160 };
161
162 const handleCancelGemini = () => {
163 setIsEditingGemini(false);
164 geminiForm.reset();
giod0026612025-05-08 13:00:36 +0000165 };
gio7f98e772025-05-07 11:00:14 +0000166
gio69ff7592025-07-03 06:27:21 +0000167 const handleCancelAnthropic = () => {
168 setIsEditingAnthropic(false);
169 anthropicForm.reset();
170 };
171
giod0026612025-05-08 13:00:36 +0000172 return (
gio02f1cad2025-05-13 11:51:55 +0000173 <div className="px-4 py-1">
174 <div className="flex flex-col gap-1">
175 <div className="flex flex-row items-center gap-1">
176 {githubService ? <CircleCheck /> : <CircleX />}
177 <div>Github</div>
giod0026612025-05-08 13:00:36 +0000178 </div>
gio3a921b82025-05-10 07:36:09 +0000179
gio69148322025-06-19 23:16:12 +0400180 {!!githubService && !isEditingGithub && (
181 <Button variant="outline" className="w-fit" onClick={() => setIsEditingGithub(true)}>
gio3a921b82025-05-10 07:36:09 +0000182 Update Access Token
183 </Button>
184 )}
185
gio69148322025-06-19 23:16:12 +0400186 {(!githubService || isEditingGithub) && (
gio02f1cad2025-05-13 11:51:55 +0000187 <div className="flex flex-row items-center gap-1 text-sm">
gio33046722025-05-16 14:49:55 +0000188 <div>
189 Follow the link to generate new PAT:{" "}
190 <a href="https://github.com/settings/personal-access-tokens" target="_blank">
191 https://github.com/settings/personal-access-tokens
192 </a>
193 <br />
194 Grant following <b>Repository</b> permissions:
195 <ul>
196 <li>
197 <b>Contents</b> - Read-only access so dodo can clone the repository
198 </li>
199 <li>
200 <b>Metadata</b> - Read-only access so dodo can search for repositories
201 </li>
202 <li>
gioeb148c82025-05-19 16:17:22 +0000203 <b>Webhooks</b> - Read and write access so dodo can register webhooks with Github
204 and receive updates
205 </li>
206 <li>
gio33046722025-05-16 14:49:55 +0000207 <b>Administration</b> - Read and write access so dodo automatically add deploy keys
208 to repositories
209 </li>
210 </ul>
211 </div>
gio02f1cad2025-05-13 11:51:55 +0000212 </div>
213 )}
gio69148322025-06-19 23:16:12 +0400214 {(!githubService || isEditingGithub) && (
215 <Form {...githubForm}>
216 <form className="space-y-2" onSubmit={githubForm.handleSubmit(onGithubSubmit)}>
gio3a921b82025-05-10 07:36:09 +0000217 <FormField
gio69148322025-06-19 23:16:12 +0400218 control={githubForm.control}
gio3a921b82025-05-10 07:36:09 +0000219 name="githubToken"
220 render={({ field }) => (
221 <FormItem>
222 <FormControl>
223 <Input
224 type="password"
gio02f1cad2025-05-13 11:51:55 +0000225 placeholder="Personal Access Token"
226 className="w-1/4"
gio3a921b82025-05-10 07:36:09 +0000227 {...field}
228 />
229 </FormControl>
230 <FormMessage />
231 </FormItem>
232 )}
233 />
gio02f1cad2025-05-13 11:51:55 +0000234 <div className="flex flex-row items-center gap-1">
gio3a921b82025-05-10 07:36:09 +0000235 <Button type="submit" disabled={isSaving}>
236 {isSaving ? "Saving..." : "Save"}
237 </Button>
238 {!!githubService && (
gio69148322025-06-19 23:16:12 +0400239 <Button
240 type="button"
241 variant="outline"
242 onClick={handleCancelGithub}
243 disabled={isSaving}
244 >
245 Cancel
246 </Button>
247 )}
248 </div>
249 </form>
250 </Form>
251 )}
252 </div>
253 <div className="flex flex-col gap-1">
254 <div className="flex flex-row items-center gap-1">
255 {geminiService ? <CircleCheck /> : <CircleX />}
256 <div>Gemini</div>
257 </div>
258
259 {!!geminiService && !isEditingGemini && (
260 <Button variant="outline" className="w-fit" onClick={() => setIsEditingGemini(true)}>
261 Update API Key
262 </Button>
263 )}
264
265 {(!geminiService || isEditingGemini) && (
266 <div className="flex flex-row items-center gap-1 text-sm">
267 <div>
268 Follow the link to generate new API Key:{" "}
269 <a href="https://aistudio.google.com/app/apikey" target="_blank">
270 https://aistudio.google.com/app/apikey
271 </a>
272 </div>
273 </div>
274 )}
275 {(!geminiService || isEditingGemini) && (
276 <Form {...geminiForm}>
277 <form className="space-y-2" onSubmit={geminiForm.handleSubmit(onGeminiSubmit)}>
278 <FormField
279 control={geminiForm.control}
280 name="geminiApiKey"
281 render={({ field }) => (
282 <FormItem>
283 <FormControl>
284 <Input
285 type="password"
286 placeholder="Gemini API Token"
287 className="w-1/4"
288 {...field}
289 />
290 </FormControl>
291 <FormMessage />
292 </FormItem>
293 )}
294 />
295 <div className="flex flex-row items-center gap-1">
296 <Button type="submit" disabled={isSaving}>
297 {isSaving ? "Saving..." : "Save"}
298 </Button>
299 {!!geminiService && (
300 <Button
301 type="button"
302 variant="outline"
303 onClick={handleCancelGemini}
304 disabled={isSaving}
305 >
gio3a921b82025-05-10 07:36:09 +0000306 Cancel
307 </Button>
308 )}
309 </div>
310 </form>
311 </Form>
312 )}
giod0026612025-05-08 13:00:36 +0000313 </div>
gio69ff7592025-07-03 06:27:21 +0000314 <div className="flex flex-col gap-1">
315 <div className="flex flex-row items-center gap-1">
316 {anthropicService ? <CircleCheck /> : <CircleX />}
317 <div>Anthropic</div>
318 </div>
319
320 {!!anthropicService && !isEditingAnthropic && (
321 <Button variant="outline" className="w-fit" onClick={() => setIsEditingAnthropic(true)}>
322 Update API Key
323 </Button>
324 )}
325
326 {(!anthropicService || isEditingAnthropic) && (
327 <div className="flex flex-row items-center gap-1 text-sm">
328 <div>
329 Follow the link to generate new API Key:{" "}
330 <a href="https://console.anthropic.com/settings/keys" target="_blank">
331 https://console.anthropic.com/settings/keys
332 </a>
333 </div>
334 </div>
335 )}
336 {(!anthropicService || isEditingAnthropic) && (
337 <Form {...anthropicForm}>
338 <form className="space-y-2" onSubmit={anthropicForm.handleSubmit(onAnthropicSubmit)}>
339 <FormField
340 control={anthropicForm.control}
341 name="anthropicApiKey"
342 render={({ field }) => (
343 <FormItem>
344 <FormControl>
345 <Input
346 type="password"
347 placeholder="Anthropic API Token"
348 className="w-1/4"
349 {...field}
350 />
351 </FormControl>
352 <FormMessage />
353 </FormItem>
354 )}
355 />
356 <div className="flex flex-row items-center gap-1">
357 <Button type="submit" disabled={isSaving}>
358 {isSaving ? "Saving..." : "Save"}
359 </Button>
360 {!!anthropicService && (
361 <Button
362 type="button"
363 variant="outline"
364 onClick={handleCancelAnthropic}
365 disabled={isSaving}
366 >
367 Cancel
368 </Button>
369 )}
370 </div>
371 </form>
372 </Form>
373 )}
374 </div>
giod0026612025-05-08 13:00:36 +0000375 </div>
376 );
377}