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/lib/categories.ts b/apps/canvas/front/src/lib/categories.ts
index 8753455..6c655e1 100644
--- a/apps/canvas/front/src/lib/categories.ts
+++ b/apps/canvas/front/src/lib/categories.ts
@@ -1,7 +1,8 @@
-import { NodeType, InitData } from "@/lib/state";
+import { NodeType, InitData } from "config";
 
 export interface CategoryItem<T extends NodeType> {
 	title: string;
+	// TODO(gio): make InitData generic
 	init: InitData;
 	type: T;
 }
@@ -102,4 +103,82 @@
 			},
 		],
 	},
+	{
+		title: "AI", // New Category for AI tools
+		items: [
+			{
+				title: "Agent Sketch",
+				init: {
+					...defaultInit,
+					label: "Agent Sketch", // Default label for the node
+					type: "sketch:latest",
+					ports: [
+						{
+							id: "agent",
+							name: "agent",
+							value: 2001,
+						},
+						{
+							id: "p8080",
+							name: "p8080",
+							value: 8080,
+						},
+						{
+							id: "p8081",
+							name: "p8081",
+							value: 8081,
+						},
+						{
+							id: "p8082",
+							name: "p8082",
+							value: 8082,
+						},
+						{
+							id: "p8083",
+							name: "p8083",
+							value: 8083,
+						},
+						{
+							id: "p8084",
+							name: "p8084",
+							value: 8084,
+						},
+					],
+					envVars: [
+						{
+							id: "agent",
+							name: "DODO_PORT_AGENT",
+							source: null,
+						},
+						{
+							id: "p8080",
+							name: "DODO_PORT_P8080",
+							source: null,
+						},
+						{
+							id: "p8081",
+							name: "DODO_PORT_P8081",
+							source: null,
+						},
+						{
+							id: "p8082",
+							name: "DODO_PORT_P8082",
+							source: null,
+						},
+						{
+							id: "p8083",
+							name: "DODO_PORT_P8083",
+							source: null,
+						},
+						{
+							id: "p8084",
+							name: "DODO_PORT_P8084",
+							source: null,
+						},
+					],
+				},
+				type: "app",
+			},
+		],
+	},
 ];
diff --git a/apps/canvas/front/src/lib/config.ts b/apps/canvas/front/src/lib/config.ts
index 39db5b4..f6dee32 100644
--- a/apps/canvas/front/src/lib/config.ts
+++ b/apps/canvas/front/src/lib/config.ts
@@ -34,7 +34,11 @@
 			return 4;
 		case "gateway-https":
 			return 5;
-		case undefined:
+		case "gateway-tcp":
+			return 7;
+		case "network":
+			return 8;
+		case undefined: // For NANode
 			return 100;
 	}
 }
@@ -153,6 +157,7 @@
 			}),
 		);
 	const noSource = apps
+		.filter((n) => n.data.type !== "sketch:latest")
 		.filter((n) => n.data == null || n.data.repository == null || n.data.repository.repoNodeId === "")
 		.map(
 			(n): Message => ({
@@ -194,44 +199,46 @@
 				onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
 			}),
 		);
-	const noIngress = apps.flatMap((n): Message[] => {
-		if (n.data == null) {
-			return [];
-		}
-		return (n.data.ports || [])
-			.filter(
-				(p) =>
-					!nodes
-						.filter((i) => i.type === "gateway-https")
-						.some((i) => {
-							if (
-								i.data &&
-								i.data.https &&
-								i.data.https.serviceId === n.id &&
-								i.data.https.portId === p.id
-							) {
-								return true;
-							}
-							return false;
-						}),
-			)
-			.map(
-				(p): Message => ({
-					id: `${n.id}-${p.id}-no-ingress`,
-					type: "WARNING",
-					nodeId: n.id,
-					message: `Connect to gateway: ${p.name} - ${p.value}`,
-					onHighlight: (store) => {
-						store.updateNode(n.id, { selected: true });
-						store.setHighlightCategory("gateways", true);
-					},
-					onLooseHighlight: (store) => {
-						store.updateNode(n.id, { selected: false });
-						store.setHighlightCategory("gateways", false);
-					},
-				}),
-			);
-	});
+	const noIngress = apps
+		.filter((n) => n.data.type !== "sketch:latest")
+		.flatMap((n): Message[] => {
+			if (n.data == null) {
+				return [];
+			}
+			return (n.data.ports || [])
+				.filter(
+					(p) =>
+						!nodes
+							.filter((i) => i.type === "gateway-https")
+							.some((i) => {
+								if (
+									i.data &&
+									i.data.https &&
+									i.data.https.serviceId === n.id &&
+									i.data.https.portId === p.id
+								) {
+									return true;
+								}
+								return false;
+							}),
+				)
+				.map(
+					(p): Message => ({
+						id: `${n.id}-${p.id}-no-ingress`,
+						type: "WARNING",
+						nodeId: n.id,
+						message: `Connect to gateway: ${p.name} - ${p.value}`,
+						onHighlight: (store) => {
+							store.updateNode(n.id, { selected: true });
+							store.setHighlightCategory("gateways", true);
+						},
+						onLooseHighlight: (store) => {
+							store.updateNode(n.id, { selected: false });
+							store.setHighlightCategory("gateways", false);
+						},
+					}),
+				);
+		});
 	const multipleIngress = apps
 		.filter((n) => n.data != null && n.data.ports != null)
 		.flatMap((n) =>
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index 8ada938..3733e56 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -46,7 +46,7 @@
 			case "volume":
 				return n.data.label || "Volume";
 			case undefined:
-				throw new Error("MUST NOT REACH!");
+				throw new Error(`nodeLabel: Node type is undefined. Node ID: ${n.id}, Data: ${JSON.stringify(n.data)}`);
 		}
 	} catch (e) {
 		console.error("opaa", e);
@@ -106,7 +106,9 @@
 			}
 			return true;
 		case undefined:
-			throw new Error("MUST NOT REACH!");
+			throw new Error(
+				`nodeIsConnectable: Node type is undefined. Node ID: ${n.id}, Handle: ${handle}, Data: ${JSON.stringify(n.data)}`,
+			);
 	}
 }
 
@@ -153,12 +155,12 @@
 };
 
 const defaultEnv: Env = {
-	managerAddr: undefined,
 	deployKeyPublic: undefined,
 	instanceId: undefined,
 	networks: [],
 	integrations: {
 		github: false,
+		gemini: false,
 	},
 	services: [],
 	user: {
@@ -225,7 +227,6 @@
 const projectIdSelector = (state: AppState) => state.projectId;
 const categoriesSelector = (state: AppState) => state.categories;
 const messagesSelector = (state: AppState) => state.messages;
-const githubServiceSelector = (state: AppState) => state.githubService;
 const envSelector = (state: AppState) => state.env;
 const zoomSelector = (state: AppState) => state.zoom;
 const githubRepositoriesSelector = (state: AppState) => state.githubRepositories;
@@ -268,8 +269,12 @@
 	return useStateStore(envSelector);
 }
 
-export function useGithubService(): GitHubService | null {
-	return useStateStore(githubServiceSelector);
+export function useGithubService(): boolean {
+	return useStateStore(envSelector).integrations.github;
+}
+
+export function useGeminiService(): boolean {
+	return useStateStore(envSelector).integrations.gemini;
 }
 
 export function useGithubRepositories(): GitHubRepository[] {
@@ -457,7 +462,9 @@
 			if (c.sourceHandle === "env_var" && c.targetHandle === "env_var") {
 				const sourceEnvVars = nodeEnvVarNames(sn);
 				if (sourceEnvVars.length === 0) {
-					throw new Error("MUST NOT REACH!");
+					throw new Error(
+						`onConnect (env_var): Source node ${sn.id} (type: ${sn.type}) has no env vars to connect from.`,
+					);
 				}
 				const id = uuidv4();
 				if (sourceEnvVars.length === 1) {
diff --git a/apps/canvas/front/src/lib/types.ts b/apps/canvas/front/src/lib/types.ts
index 983b13e..09d1ba0 100644
--- a/apps/canvas/front/src/lib/types.ts
+++ b/apps/canvas/front/src/lib/types.ts
@@ -1,4 +1,4 @@
-import { AppNode } from "./state";
+import { AppNode } from "config";
 
 export interface NodeDetailsProps<T extends AppNode = AppNode> {
 	node: T;