diff --git a/apps/rowboat/app/actions/project_actions.ts b/apps/rowboat/app/actions/project_actions.ts index e0750696..f22a3c0a 100644 --- a/apps/rowboat/app/actions/project_actions.ts +++ b/apps/rowboat/app/actions/project_actions.ts @@ -13,6 +13,7 @@ import { Project } from "../lib/types/project_types"; import { USE_AUTH } from "../lib/feature_flags"; import { deleteMcpServerInstance, listActiveServerInstances } from "./klavis_actions"; import { authorizeUserAction } from "./billing_actions"; +import { Workflow } from "../lib/types/workflow_types"; const KLAVIS_API_KEY = process.env.KLAVIS_API_KEY || ''; @@ -311,3 +312,35 @@ export async function createProjectFromPrompt(formData: FormData): Promise<{ id: return { id: projectId }; } + +export async function createProjectFromWorkflowJson(formData: FormData): Promise<{ id: string } | { billingError: string }> { + const user = await authCheck(); + const workflowJson = formData.get('workflowJson') as string; + let workflowData; + try { + workflowData = JSON.parse(workflowJson); + } catch (e) { + throw new Error('Invalid JSON'); + } + // Validate and parse with zod + const parsed = Workflow.omit({ projectId: true }).safeParse(workflowData); + if (!parsed.success) { + throw new Error('Invalid workflow JSON: ' + JSON.stringify(parsed.error.issues)); + } + const workflow = parsed.data; + const name = workflow.name || 'Imported Project'; + const response = await createBaseProject(name, user); + if ('billingError' in response) { + return response; + } + const projectId = response.id; + const now = new Date().toISOString(); + await agentWorkflowsCollection.insertOne({ + ...workflow, + projectId, + createdAt: now, + lastUpdatedAt: now, + name: workflow.name || 'Version 1', + }); + return { id: projectId }; +} diff --git a/apps/rowboat/app/projects/select/components/create-project.tsx b/apps/rowboat/app/projects/select/components/create-project.tsx index ee785cd6..a461243f 100644 --- a/apps/rowboat/app/projects/select/components/create-project.tsx +++ b/apps/rowboat/app/projects/select/components/create-project.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState, useRef } from "react"; -import { createProject, createProjectFromPrompt } from "@/app/actions/project_actions"; +import { createProject, createProjectFromPrompt, createProjectFromWorkflowJson } from "@/app/actions/project_actions"; import { useRouter } from 'next/navigation'; import clsx from 'clsx'; import { starting_copilot_prompts } from "@/app/lib/project_templates"; @@ -14,6 +14,10 @@ import { USE_MULTIPLE_PROJECTS } from "@/app/lib/feature_flags"; import { HorizontalDivider } from "@/components/ui/horizontal-divider"; import { Tooltip } from "@heroui/react"; import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal"; +import { z } from 'zod'; +import { Workflow } from '@/app/lib/types/workflow_types'; +import { Modal } from '@/components/ui/modal'; +import { PlusIcon } from "lucide-react"; // Add glow animation styles const glowStyles = ` @@ -61,7 +65,8 @@ const glowStyles = ` const TabType = { Describe: 'describe', Blank: 'blank', - Example: 'example' + Example: 'example', + Import: 'import', } as const; type TabState = typeof TabType[keyof typeof TabType]; @@ -139,7 +144,11 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe const [name, setName] = useState(defaultName); const [promptError, setPromptError] = useState(null); const [billingError, setBillingError] = useState(null); + const [importJson, setImportJson] = useState(""); + const [importError, setImportError] = useState(null); + const [importModalOpen, setImportModalOpen] = useState(false); const router = useRouter(); + const [importLoading, setImportLoading] = useState(false); // Add this effect to update name when defaultName changes useEffect(() => { @@ -177,11 +186,13 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe const handleTabChange = (tab: TabState) => { setSelectedTab(tab); setIsExamplesDropdownOpen(false); - + setImportError(null); if (tab === TabType.Blank) { setCustomPrompt(''); } else if (tab === TabType.Describe) { setCustomPrompt(''); + } else if (tab === TabType.Import) { + setImportJson(''); } }; @@ -230,6 +241,43 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe } } + async function handleImportSubmit(e?: React.FormEvent) { + if (e) e.preventDefault(); + setImportError(null); + setImportLoading(true); + let parsed; + try { + const json = JSON.parse(importJson); + parsed = Workflow.omit({ projectId: true }).safeParse(json); + if (!parsed.success) { + setImportError('Invalid workflow JSON: ' + JSON.stringify(parsed.error.issues)); + setImportModalOpen(true); + setImportLoading(false); + return; + } + } catch (err) { + setImportError('Invalid JSON: ' + (err instanceof Error ? err.message : String(err))); + setImportModalOpen(true); + setImportLoading(false); + return; + } + try { + const formData = new FormData(); + formData.append('workflowJson', importJson); + const response = await createProjectFromWorkflowJson(formData); + if ('id' in response) { + router.push(`/projects/${response.id}/workflow`); + } else { + setBillingError(response.billingError); + } + } catch (err) { + setImportError('Failed to import: ' + (err instanceof Error ? err.message : String(err))); + setImportModalOpen(true); + } finally { + setImportLoading(false); + } + } + return ( <>
{/* Tab Section */} @@ -341,9 +389,61 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
)} + + {/* Import JSON Section */} + {selectedTab === TabType.Import && ( +
+
+ +