import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Background, BackgroundVariant, Controls, MiniMap, ReactFlow, type Connection, type Edge, type Node, type NodeChange, type ReactFlowInstance, type Viewport, } from "@xyflow/react"; import { ApiClient } from "./api-client.ts"; import { buildCustomNodeEnvelopePreview, type CustomNodeValidationIssue, validateCustomNodeDefinition, } from "../../../../packages/contracts/src/custom-node.ts"; import { formatCustomNodeInputModeKey, formatCustomNodeOutputModeKey, formatCustomNodeSourceKindKey, } from "./custom-node-presenter.ts"; import { localizeNodeDefinition, type TranslationKey, useI18n, } from "./i18n.tsx"; import { addNodeToDraft, addNodeToDraftAtPosition, canConnectNodesInDraft, connectNodesInDraft, createDefaultWorkflowDraft, getNodeRuntimeConfig, removeNodeFromDraft, setNodePosition, resolveDefinitionIdForNode, serializeWorkflowDraft, setViewportInDraft, setNodeRuntimeConfig, workflowDraftFromVersion, type WorkflowConnectionValidationReason, type WorkflowDraft, type WorkflowNodeRuntimeConfig, } from "./workflow-editor-state.ts"; const NODE_LIBRARY_MIME = "application/x-emboflow-node-definition"; const ACTIVE_PROJECT_STORAGE_KEY_PREFIX = "emboflow.activeProject"; function navigateTo(pathname: string) { if (typeof window === "undefined") { return; } if (window.location.pathname === pathname) { return; } window.history.pushState({}, "", pathname); window.dispatchEvent(new PopStateEvent("popstate")); } function getActiveProjectStorageKey(workspaceId: string) { return `${ACTIVE_PROJECT_STORAGE_KEY_PREFIX}:${workspaceId}`; } function normalizePathnameForProjectSwitch(pathname: string) { if (pathname.startsWith("/assets/")) { return "/assets"; } if (pathname.startsWith("/nodes")) { return "/nodes"; } if (pathname.startsWith("/workflows/")) { return "/workflows"; } if (pathname.startsWith("/runs/")) { return "/runs"; } if (pathname.startsWith("/explore/")) { return "/explore"; } return pathname === "/" ? "/projects" : pathname; } function mapConnectionValidationReasonToKey( reason: WorkflowConnectionValidationReason | "missing_connection_endpoint", ): TranslationKey { switch (reason) { case "missing_source": case "missing_target": case "missing_connection_endpoint": return "invalidConnectionMissingEndpoint"; case "self": return "invalidConnectionSelf"; case "duplicate": return "invalidConnectionDuplicate"; case "source_disallows_outgoing": return "invalidConnectionSourceDisallowsOutgoing"; case "target_disallows_incoming": return "invalidConnectionTargetDisallowsIncoming"; case "target_already_has_incoming": return "invalidConnectionTargetAlreadyHasIncoming"; case "cycle": return "invalidConnectionCycle"; } } function mapCustomNodeValidationIssueToKey(issue: CustomNodeValidationIssue): TranslationKey { switch (issue) { case "name_required": return "customNodeValidationNameRequired"; case "name_too_long": return "customNodeValidationNameTooLong"; case "invalid_category": return "customNodeValidationInvalidCategory"; case "invalid_source_kind": return "customNodeValidationInvalidSourceKind"; case "image_required": return "customNodeValidationImageRequired"; case "dockerfile_required": return "customNodeValidationDockerfileRequired"; case "dockerfile_missing_from": return "customNodeValidationDockerfileMissingFrom"; case "invalid_command": return "customNodeValidationInvalidCommand"; case "invalid_input_mode": return "customNodeValidationInvalidInputMode"; case "invalid_output_mode": return "customNodeValidationInvalidOutputMode"; case "invalid_artifact_type": return "customNodeValidationInvalidArtifactType"; case "source_cannot_be_multi_input": return "customNodeValidationSourceCannotBeMultiInput"; } } type NavItem = "Projects" | "Assets" | "Nodes" | "Workflows" | "Runs" | "Explore" | "Labels" | "Admin"; type BootstrapContext = { userId: string; workspace: { _id: string; name: string }; project: { _id: string; name: string }; }; type ProjectSummary = { _id: string; name: string; description?: string; status?: string; createdAt?: string; }; type AppProps = { apiBaseUrl: string; }; type WorkflowPreflightResult = { ok: boolean; issues: Array<{ severity: "error" | "warning"; code: string; message: string; nodeId?: string; nodeDefinitionId?: string; }>; summary: { errorCount: number; warningCount: number; }; }; function translateStatus(status: string | undefined, t: ReturnType["t"]) { switch (status) { case "success": return t("success"); case "failed": return t("failed"); case "running": return t("running"); case "queued": case "pending": return t("queued"); case "cancelled": return t("cancelled"); default: return status ?? "unknown"; } } function formatTaskSummary(task: any, t: ReturnType["t"]) { if (task?.summary?.errorMessage) { return task.summary.errorMessage; } const outcome = translateStatus(task?.summary?.outcome ?? task?.status, t); const executor = task?.summary?.executorType ?? task?.executorType ?? "unknown"; const assetCount = task?.summary?.assetCount ?? task?.assetIds?.length ?? 0; const artifactCount = task?.summary?.artifactIds?.length ?? task?.outputArtifactIds?.length ?? 0; return `${t("viaExecutor", { outcome, executor })}; ${t("assetCount", { count: assetCount })}; ${t("artifactCount", { count: artifactCount })}`; } function formatRunSummary(run: any, t: ReturnType["t"]) { const totalTaskCount = run?.summary?.totalTaskCount ?? 0; const successCount = run?.summary?.taskCounts?.success ?? 0; const failedCount = run?.summary?.taskCounts?.failed ?? 0; const runningCount = run?.summary?.taskCounts?.running ?? 0; const cancelledCount = run?.summary?.taskCounts?.cancelled ?? 0; const stdoutLineCount = run?.summary?.stdoutLineCount ?? 0; const stderrLineCount = run?.summary?.stderrLineCount ?? 0; return [ t("successCount", { count: successCount }), t("failedCount", { count: failedCount }), t("runningCount", { count: runningCount }), t("cancelledCount", { count: cancelledCount }), t("stdoutLines", { count: stdoutLineCount }), t("stderrLines", { count: stderrLineCount }), t("totalTasks", { count: totalTaskCount }), ].join(", "); } function formatExecutorConfigLabel(config?: Record) { if (!config || Object.keys(config).length === 0) { return "none"; } return JSON.stringify(config); } function getDefaultExecutorType(definition?: { defaultExecutorType?: "python" | "docker" | "http" } | null) { return definition?.defaultExecutorType ?? "python"; } function getDefaultExecutorConfig(definition?: { defaultExecutorConfig?: Record } | null) { return definition?.defaultExecutorConfig ? { ...definition.defaultExecutorConfig } : undefined; } function getEffectiveNodeRuntimeConfig( definition: { id: string; defaultExecutorType?: "python" | "docker" | "http"; defaultExecutorConfig?: Record } | null, runtimeConfig: WorkflowNodeRuntimeConfig | undefined, ): WorkflowNodeRuntimeConfig { const executorType = runtimeConfig?.executorType ?? getDefaultExecutorType(definition); const executorConfig = { ...(getDefaultExecutorConfig(definition) ?? {}), ...(runtimeConfig?.executorConfig ?? {}), }; return { definitionId: runtimeConfig?.definitionId ?? definition?.id, executorType, executorConfig: Object.keys(executorConfig).length > 0 ? executorConfig : undefined, codeHookSpec: runtimeConfig?.codeHookSpec, artifactType: runtimeConfig?.artifactType, artifactTitle: runtimeConfig?.artifactTitle, }; } function usePathname() { const [pathname, setPathname] = useState( typeof window === "undefined" ? "/projects" : window.location.pathname || "/projects", ); useEffect(() => { const handle = () => setPathname(window.location.pathname || "/assets"); window.addEventListener("popstate", handle); return () => window.removeEventListener("popstate", handle); }, []); return pathname === "/" ? "/projects" : pathname; } function AppShell(props: { workspaceName: string; projectName: string; projectControl?: React.ReactNode; active: NavItem; children: React.ReactNode; }) { const { language, setLanguage, t } = useI18n(); const navItems: Array<{ label: NavItem; href: string; key: "navProjects" | "navAssets" | "navNodes" | "navWorkflows" | "navRuns" | "navExplore" | "navLabels" | "navAdmin" }> = [ { label: "Projects", href: "/projects", key: "navProjects" }, { label: "Assets", href: "/assets", key: "navAssets" }, { label: "Nodes", href: "/nodes", key: "navNodes" }, { label: "Workflows", href: "/workflows", key: "navWorkflows" }, { label: "Runs", href: "/runs", key: "navRuns" }, { label: "Explore", href: "/explore", key: "navExplore" }, { label: "Labels", href: "/labels", key: "navLabels" }, { label: "Admin", href: "/admin", key: "navAdmin" }, ]; return (
{t("workspace")} {props.workspaceName}
{t("project")} {props.projectControl ?? {props.projectName}}
{t("runs")} {t("localDev")}
{props.children}
); } function ProjectsPage(props: { api: ApiClient; bootstrap: BootstrapContext; projects: ProjectSummary[]; activeProjectId: string; onProjectCreated: (project: ProjectSummary) => Promise | void; onProjectSelected: (projectId: string, nextPath?: string) => void; }) { const { t } = useI18n(); const [projectName, setProjectName] = useState(""); const [projectDescription, setProjectDescription] = useState(""); const [error, setError] = useState(null); return (

{t("projectsTitle")}

{t("projectsDescription")}