| 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> |
| ); |
| } |