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/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index 7eb632c..fa08977 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -1,7 +1,7 @@
 import { v4 as uuidv4 } from "uuid";
 import { NodeRect } from "./node-rect";
 import { useStateStore, nodeLabel, AppState, nodeIsConnectable, useEnv, useGithubRepositories } from "@/lib/state";
-import { ServiceNode, ServiceTypes } from "config";
+import { ServiceNode, ServiceTypes, GatewayHttpsNode, GatewayTCPNode, BoundEnvVar, AppNode, GithubNode } from "config";
 import { KeyboardEvent, FocusEvent, useCallback, useEffect, useMemo, useState } from "react";
 import { z } from "zod";
 import { useForm, EventType, DeepPartial } from "react-hook-form";
@@ -27,7 +27,7 @@
 	const isConnectablePorts = useMemo(() => nodeIsConnectable(node, "ports"), [node]);
 	const isConnectableRepository = useMemo(() => nodeIsConnectable(node, "repository"), [node]);
 	return (
-		<NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
+		<NodeRect id={id} selected={selected} node={node} state={node.data.state}>
 			<div style={{ padding: "10px 20px" }}>
 				{nodeLabel(node)}
 				<Handle
@@ -79,6 +79,10 @@
 	subdomain: z.string().min(1, "required"),
 });
 
+const agentSchema = z.object({
+	geminiApiKey: z.string().optional(),
+});
+
 export function NodeAppDetails({ node, disabled, showName = true, isOverview = false }: NodeDetailsProps<ServiceNode>) {
 	const { data } = node;
 	return (
@@ -146,22 +150,24 @@
 							</TooltipProvider>
 						)}
 					</TabsTrigger>
-					<TabsTrigger value="dev">
-						{isOverview ? (
-							<div className="flex flex-row gap-1 items-center">
-								<Code /> Dev
-							</div>
-						) : (
-							<TooltipProvider>
-								<Tooltip>
-									<TooltipTrigger className="flex flex-row gap-1 items-center">
-										<Code />
-									</TooltipTrigger>
-									<TooltipContent>Dev</TooltipContent>
-								</Tooltip>
-							</TooltipProvider>
-						)}
-					</TabsTrigger>
+					{node.data.type !== "sketch:latest" && (
+						<TabsTrigger value="dev">
+							{isOverview ? (
+								<div className="flex flex-row gap-1 items-center">
+									<Code /> Dev
+								</div>
+							) : (
+								<TooltipProvider>
+									<Tooltip>
+										<TooltipTrigger className="flex flex-row gap-1 items-center">
+											<Code />
+										</TooltipTrigger>
+										<TooltipContent>Dev</TooltipContent>
+									</Tooltip>
+								</TooltipProvider>
+							)}
+						</TabsTrigger>
+					)}
 				</TabsList>
 				<TabsContent value="runtime">
 					<Runtime node={node} disabled={disabled} />
@@ -172,9 +178,11 @@
 				<TabsContent value="vars">
 					<EnvVars node={node} disabled={disabled} />
 				</TabsContent>
-				<TabsContent value="dev">
-					<Dev node={node} disabled={disabled} />
-				</TabsContent>
+				{node.data.type !== "sketch:latest" && (
+					<TabsContent value="dev">
+						<Dev node={node} disabled={disabled} />
+					</TabsContent>
+				)}
 			</Tabs>
 		</>
 	);
@@ -241,49 +249,97 @@
 		},
 		[id, store],
 	);
+	const agentForm = useForm<z.infer<typeof agentSchema>>({
+		resolver: zodResolver(agentSchema),
+		mode: "onChange",
+		defaultValues: {
+			geminiApiKey: data.agent?.geminiApiKey,
+		},
+	});
+	useEffect(() => {
+		const sub = agentForm.watch((value) => {
+			store.updateNodeData<"app">(id, {
+				agent: {
+					geminiApiKey: value.geminiApiKey,
+				},
+			});
+		});
+		return () => sub.unsubscribe();
+	}, [id, agentForm, store]);
 	return (
 		<>
 			<SourceRepo node={node} disabled={disabled} />
-			<Form {...form}>
-				<form className="space-y-2">
-					<Label>Container Image</Label>
-					<FormField
-						control={form.control}
-						name="type"
-						render={({ field }) => (
-							<FormItem>
-								<Select
-									onValueChange={field.onChange}
-									value={field.value || ""}
-									{...typeProps}
-									disabled={disabled}
-								>
+			{node.data.type !== "sketch:latest" && (
+				<Form {...form}>
+					<form className="space-y-2">
+						<Label>Container Image</Label>
+						<FormField
+							control={form.control}
+							name="type"
+							render={({ field }) => (
+								<FormItem>
+									<Select
+										onValueChange={field.onChange}
+										value={field.value || ""}
+										{...typeProps}
+										disabled={disabled}
+									>
+										<FormControl>
+											<SelectTrigger>
+												<SelectValue />
+											</SelectTrigger>
+										</FormControl>
+										<SelectContent>
+											{ServiceTypes.filter((t) => t !== "sketch:latest").map((t) => (
+												<SelectItem key={t} value={t}>
+													{t}
+												</SelectItem>
+											))}
+										</SelectContent>
+									</Select>
+									<FormMessage />
+								</FormItem>
+							)}
+						/>
+					</form>
+				</Form>
+			)}
+			{node.data.type === "sketch:latest" && (
+				<Form {...agentForm}>
+					<form className="space-y-2">
+						<Label>Gemini API Key</Label>
+						<FormField
+							control={agentForm.control}
+							name="geminiApiKey"
+							render={({ field }) => (
+								<FormItem>
 									<FormControl>
-										<SelectTrigger>
-											<SelectValue />
-										</SelectTrigger>
+										<Input
+											type="password"
+											placeholder="Override Gemini API key"
+											{...field}
+											value={field.value || ""}
+											disabled={disabled}
+										/>
 									</FormControl>
-									<SelectContent>
-										{ServiceTypes.map((t) => (
-											<SelectItem key={t} value={t}>
-												{t}
-											</SelectItem>
-										))}
-									</SelectContent>
-								</Select>
-								<FormMessage />
-							</FormItem>
-						)}
+									<FormMessage />
+								</FormItem>
+							)}
+						/>
+					</form>
+				</Form>
+			)}
+			{node.data.type !== "sketch:latest" && (
+				<>
+					<Label>Pre-Build Commands</Label>
+					<Textarea
+						placeholder="new line separated list of commands to run before running the service"
+						value={data.preBuildCommands}
+						onChange={setPreBuildCommands}
+						disabled={disabled}
 					/>
-				</form>
-			</Form>
-			<Label>Pre-Build Commands</Label>
-			<Textarea
-				placeholder="new line separated list of commands to run before running the service"
-				value={data.preBuildCommands}
-				onChange={setPreBuildCommands}
-				disabled={disabled}
-			/>
+				</>
+			)}
 		</>
 	);
 }