blob: 09bc28df54e3d80260e1cbf6586a1f93a059d94e [file] [log] [blame]
import { useProjectId, useGithubService, useStateStore, useGeminiService, useAnthropicService } from "@/lib/state";
import { Form, FormControl, FormField, FormItem, FormMessage } from "./components/ui/form";
import { Input } from "./components/ui/input";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "./components/ui/button";
import { useToast } from "@/hooks/use-toast";
import { CircleCheck, CircleX } from "lucide-react";
import { useState, useCallback } from "react";
const githubSchema = z.object({
githubToken: z.string().min(1, "GitHub token is required"),
});
const geminiSchema = z.object({
geminiApiKey: z.string().min(1, "Gemini API token is required"),
});
const anthropicSchema = z.object({
anthropicApiKey: z.string().min(1, "Anthropic API token is required"),
});
export function Integrations() {
const { toast } = useToast();
const store = useStateStore();
const projectId = useProjectId();
const [isEditingGithub, setIsEditingGithub] = useState(false);
const [isEditingGemini, setIsEditingGemini] = useState(false);
const [isEditingAnthropic, setIsEditingAnthropic] = useState(false);
const githubService = useGithubService();
const geminiService = useGeminiService();
const anthropicService = useAnthropicService();
const [isSaving, setIsSaving] = useState(false);
const githubForm = useForm<z.infer<typeof githubSchema>>({
resolver: zodResolver(githubSchema),
mode: "onChange",
});
const geminiForm = useForm<z.infer<typeof geminiSchema>>({
resolver: zodResolver(geminiSchema),
mode: "onChange",
});
const anthropicForm = useForm<z.infer<typeof anthropicSchema>>({
resolver: zodResolver(anthropicSchema),
mode: "onChange",
});
const onGithubSubmit = useCallback(
async (data: z.infer<typeof githubSchema>) => {
if (!projectId) return;
setIsSaving(true);
try {
const response = await fetch(`/api/project/${projectId}/github-token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ githubToken: data.githubToken }),
});
if (!response.ok) {
throw new Error("Failed to save GitHub token");
}
await store.refreshEnv();
setIsEditingGithub(false);
githubForm.reset();
toast({
title: "GitHub token saved successfully",
});
} catch (error) {
toast({
variant: "destructive",
title: "Failed to save GitHub token",
description: error instanceof Error ? error.message : "Unknown error",
});
} finally {
setIsSaving(false);
}
},
[projectId, store, githubForm, toast, setIsEditingGithub, setIsSaving],
);
const onGeminiSubmit = useCallback(
async (data: z.infer<typeof geminiSchema>) => {
if (!projectId) return;
setIsSaving(true);
try {
const response = await fetch(`/api/project/${projectId}/gemini-token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ geminiApiKey: data.geminiApiKey }),
});
if (!response.ok) {
throw new Error("Failed to save Gemini token");
}
await store.refreshEnv();
setIsEditingGemini(false);
geminiForm.reset();
toast({
title: "Gemini token saved successfully",
});
} catch (error) {
toast({
variant: "destructive",
title: "Failed to save Gemini token",
description: error instanceof Error ? error.message : "Unknown error",
});
} finally {
setIsSaving(false);
}
},
[projectId, store, geminiForm, toast, setIsEditingGemini, setIsSaving],
);
const onAnthropicSubmit = useCallback(
async (data: z.infer<typeof anthropicSchema>) => {
if (!projectId) return;
setIsSaving(true);
try {
const response = await fetch(`/api/project/${projectId}/anthropic-token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ anthropicApiKey: data.anthropicApiKey }),
});
if (!response.ok) {
throw new Error("Failed to save Anthropic token");
}
await store.refreshEnv();
setIsEditingAnthropic(false);
anthropicForm.reset();
toast({
title: "Anthropic token saved successfully",
});
} catch (error) {
toast({
variant: "destructive",
title: "Failed to save Anthropic token",
description: error instanceof Error ? error.message : "Unknown error",
});
} finally {
setIsSaving(false);
}
},
[projectId, store, anthropicForm, toast, setIsEditingAnthropic, setIsSaving],
);
const handleCancelGithub = () => {
setIsEditingGithub(false);
githubForm.reset();
};
const handleCancelGemini = () => {
setIsEditingGemini(false);
geminiForm.reset();
};
const handleCancelAnthropic = () => {
setIsEditingAnthropic(false);
anthropicForm.reset();
};
return (
<div className="px-4 py-1">
<div className="flex flex-col gap-1">
<div className="flex flex-row items-center gap-1">
{githubService ? <CircleCheck /> : <CircleX />}
<div>Github</div>
</div>
{!!githubService && !isEditingGithub && (
<Button variant="outline" className="w-fit" onClick={() => setIsEditingGithub(true)}>
Update Access Token
</Button>
)}
{(!githubService || isEditingGithub) && (
<div className="flex flex-row items-center gap-1 text-sm">
<div>
Follow the link to generate new PAT:{" "}
<a href="https://github.com/settings/personal-access-tokens" target="_blank">
https://github.com/settings/personal-access-tokens
</a>
<br />
Grant following <b>Repository</b> permissions:
<ul>
<li>
<b>Contents</b> - Read-only access so dodo can clone the repository
</li>
<li>
<b>Metadata</b> - Read-only access so dodo can search for repositories
</li>
<li>
<b>Webhooks</b> - Read and write access so dodo can register webhooks with Github
and receive updates
</li>
<li>
<b>Administration</b> - Read and write access so dodo automatically add deploy keys
to repositories
</li>
</ul>
</div>
</div>
)}
{(!githubService || isEditingGithub) && (
<Form {...githubForm}>
<form className="space-y-2" onSubmit={githubForm.handleSubmit(onGithubSubmit)}>
<FormField
control={githubForm.control}
name="githubToken"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
type="password"
placeholder="Personal Access Token"
className="w-1/4"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-row items-center gap-1">
<Button type="submit" disabled={isSaving}>
{isSaving ? "Saving..." : "Save"}
</Button>
{!!githubService && (
<Button
type="button"
variant="outline"
onClick={handleCancelGithub}
disabled={isSaving}
>
Cancel
</Button>
)}
</div>
</form>
</Form>
)}
</div>
<div className="flex flex-col gap-1">
<div className="flex flex-row items-center gap-1">
{geminiService ? <CircleCheck /> : <CircleX />}
<div>Gemini</div>
</div>
{!!geminiService && !isEditingGemini && (
<Button variant="outline" className="w-fit" onClick={() => setIsEditingGemini(true)}>
Update API Key
</Button>
)}
{(!geminiService || isEditingGemini) && (
<div className="flex flex-row items-center gap-1 text-sm">
<div>
Follow the link to generate new API Key:{" "}
<a href="https://aistudio.google.com/app/apikey" target="_blank">
https://aistudio.google.com/app/apikey
</a>
</div>
</div>
)}
{(!geminiService || isEditingGemini) && (
<Form {...geminiForm}>
<form className="space-y-2" onSubmit={geminiForm.handleSubmit(onGeminiSubmit)}>
<FormField
control={geminiForm.control}
name="geminiApiKey"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
type="password"
placeholder="Gemini API Token"
className="w-1/4"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-row items-center gap-1">
<Button type="submit" disabled={isSaving}>
{isSaving ? "Saving..." : "Save"}
</Button>
{!!geminiService && (
<Button
type="button"
variant="outline"
onClick={handleCancelGemini}
disabled={isSaving}
>
Cancel
</Button>
)}
</div>
</form>
</Form>
)}
</div>
<div className="flex flex-col gap-1">
<div className="flex flex-row items-center gap-1">
{anthropicService ? <CircleCheck /> : <CircleX />}
<div>Anthropic</div>
</div>
{!!anthropicService && !isEditingAnthropic && (
<Button variant="outline" className="w-fit" onClick={() => setIsEditingAnthropic(true)}>
Update API Key
</Button>
)}
{(!anthropicService || isEditingAnthropic) && (
<div className="flex flex-row items-center gap-1 text-sm">
<div>
Follow the link to generate new API Key:{" "}
<a href="https://console.anthropic.com/settings/keys" target="_blank">
https://console.anthropic.com/settings/keys
</a>
</div>
</div>
)}
{(!anthropicService || isEditingAnthropic) && (
<Form {...anthropicForm}>
<form className="space-y-2" onSubmit={anthropicForm.handleSubmit(onAnthropicSubmit)}>
<FormField
control={anthropicForm.control}
name="anthropicApiKey"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
type="password"
placeholder="Anthropic API Token"
className="w-1/4"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-row items-center gap-1">
<Button type="submit" disabled={isSaving}>
{isSaving ? "Saving..." : "Save"}
</Button>
{!!anthropicService && (
<Button
type="button"
variant="outline"
onClick={handleCancelAnthropic}
disabled={isSaving}
>
Cancel
</Button>
)}
</div>
</form>
</Form>
)}
</div>
</div>
);
}