Canvas: Implement Agent Sketch node, update dodo-app.jsonschema
- Add Gemini API key to the project
- Update dodo schema to support Gemini API key
- Update dodo schema to support Agent Sketch node
Change-Id: I6a96186f86ad169152ca0021b38130e485ebbf14
diff --git a/apps/canvas/front/src/Integrations.tsx b/apps/canvas/front/src/Integrations.tsx
index 722c43c..cf4b586 100644
--- a/apps/canvas/front/src/Integrations.tsx
+++ b/apps/canvas/front/src/Integrations.tsx
@@ -1,4 +1,4 @@
-import { useProjectId, useGithubService, useStateStore } from "@/lib/state";
+import { useProjectId, useGithubService, useStateStore, useGeminiService } 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";
@@ -9,25 +9,36 @@
import { CircleCheck, CircleX } from "lucide-react";
import { useState, useCallback } from "react";
-const schema = z.object({
+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"),
+});
+
export function Integrations() {
const { toast } = useToast();
const store = useStateStore();
const projectId = useProjectId();
- const [isEditing, setIsEditing] = useState(false);
+ const [isEditingGithub, setIsEditingGithub] = useState(false);
+ const [isEditingGemini, setIsEditingGemini] = useState(false);
const githubService = useGithubService();
+ const geminiService = useGeminiService();
const [isSaving, setIsSaving] = useState(false);
- const form = useForm<z.infer<typeof schema>>({
- resolver: zodResolver(schema),
+ const githubForm = useForm<z.infer<typeof githubSchema>>({
+ resolver: zodResolver(githubSchema),
mode: "onChange",
});
- const onSubmit = useCallback(
- async (data: z.infer<typeof schema>) => {
+ const geminiForm = useForm<z.infer<typeof geminiSchema>>({
+ resolver: zodResolver(geminiSchema),
+ mode: "onChange",
+ });
+
+ const onGithubSubmit = useCallback(
+ async (data: z.infer<typeof githubSchema>) => {
if (!projectId) return;
setIsSaving(true);
@@ -46,8 +57,8 @@
}
await store.refreshEnv();
- setIsEditing(false);
- form.reset();
+ setIsEditingGithub(false);
+ githubForm.reset();
toast({
title: "GitHub token saved successfully",
});
@@ -61,12 +72,51 @@
setIsSaving(false);
}
},
- [projectId, store, form, toast, setIsEditing, setIsSaving],
+ [projectId, store, githubForm, toast, setIsEditingGithub, setIsSaving],
);
- const handleCancel = () => {
- setIsEditing(false);
- form.reset();
+ 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 handleCancelGithub = () => {
+ setIsEditingGithub(false);
+ githubForm.reset();
+ };
+
+ const handleCancelGemini = () => {
+ setIsEditingGemini(false);
+ geminiForm.reset();
};
return (
@@ -77,13 +127,13 @@
<div>Github</div>
</div>
- {!!githubService && !isEditing && (
- <Button variant="outline" className="w-fit" onClick={() => setIsEditing(true)}>
+ {!!githubService && !isEditingGithub && (
+ <Button variant="outline" className="w-fit" onClick={() => setIsEditingGithub(true)}>
Update Access Token
</Button>
)}
- {(!githubService || isEditing) && (
+ {(!githubService || isEditingGithub) && (
<div className="flex flex-row items-center gap-1 text-sm">
<div>
Follow the link to generate new PAT:{" "}
@@ -111,11 +161,11 @@
</div>
</div>
)}
- {(!githubService || isEditing) && (
- <Form {...form}>
- <form className="space-y-2" onSubmit={form.handleSubmit(onSubmit)}>
+ {(!githubService || isEditingGithub) && (
+ <Form {...githubForm}>
+ <form className="space-y-2" onSubmit={githubForm.handleSubmit(onGithubSubmit)}>
<FormField
- control={form.control}
+ control={githubForm.control}
name="githubToken"
render={({ field }) => (
<FormItem>
@@ -136,7 +186,73 @@
{isSaving ? "Saving..." : "Save"}
</Button>
{!!githubService && (
- <Button type="button" variant="outline" onClick={handleCancel} disabled={isSaving}>
+ <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>
)}