mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-09 07:12:39 +02:00
Add JSON (paste) import functionality
This commit is contained in:
parent
17a033ef15
commit
a09004a635
2 changed files with 152 additions and 8 deletions
|
|
@ -13,6 +13,7 @@ import { Project } from "../lib/types/project_types";
|
||||||
import { USE_AUTH } from "../lib/feature_flags";
|
import { USE_AUTH } from "../lib/feature_flags";
|
||||||
import { deleteMcpServerInstance, listActiveServerInstances } from "./klavis_actions";
|
import { deleteMcpServerInstance, listActiveServerInstances } from "./klavis_actions";
|
||||||
import { authorizeUserAction } from "./billing_actions";
|
import { authorizeUserAction } from "./billing_actions";
|
||||||
|
import { Workflow } from "../lib/types/workflow_types";
|
||||||
|
|
||||||
const KLAVIS_API_KEY = process.env.KLAVIS_API_KEY || '';
|
const KLAVIS_API_KEY = process.env.KLAVIS_API_KEY || '';
|
||||||
|
|
||||||
|
|
@ -311,3 +312,35 @@ export async function createProjectFromPrompt(formData: FormData): Promise<{ id:
|
||||||
|
|
||||||
return { id: projectId };
|
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 };
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState, useRef } from "react";
|
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 { useRouter } from 'next/navigation';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { starting_copilot_prompts } from "@/app/lib/project_templates";
|
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 { HorizontalDivider } from "@/components/ui/horizontal-divider";
|
||||||
import { Tooltip } from "@heroui/react";
|
import { Tooltip } from "@heroui/react";
|
||||||
import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
|
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
|
// Add glow animation styles
|
||||||
const glowStyles = `
|
const glowStyles = `
|
||||||
|
|
@ -61,7 +65,8 @@ const glowStyles = `
|
||||||
const TabType = {
|
const TabType = {
|
||||||
Describe: 'describe',
|
Describe: 'describe',
|
||||||
Blank: 'blank',
|
Blank: 'blank',
|
||||||
Example: 'example'
|
Example: 'example',
|
||||||
|
Import: 'import',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type TabState = typeof TabType[keyof typeof TabType];
|
type TabState = typeof TabType[keyof typeof TabType];
|
||||||
|
|
@ -139,7 +144,11 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
|
||||||
const [name, setName] = useState(defaultName);
|
const [name, setName] = useState(defaultName);
|
||||||
const [promptError, setPromptError] = useState<string | null>(null);
|
const [promptError, setPromptError] = useState<string | null>(null);
|
||||||
const [billingError, setBillingError] = useState<string | null>(null);
|
const [billingError, setBillingError] = useState<string | null>(null);
|
||||||
|
const [importJson, setImportJson] = useState("");
|
||||||
|
const [importError, setImportError] = useState<string | null>(null);
|
||||||
|
const [importModalOpen, setImportModalOpen] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [importLoading, setImportLoading] = useState(false);
|
||||||
|
|
||||||
// Add this effect to update name when defaultName changes
|
// Add this effect to update name when defaultName changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -177,11 +186,13 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
|
||||||
const handleTabChange = (tab: TabState) => {
|
const handleTabChange = (tab: TabState) => {
|
||||||
setSelectedTab(tab);
|
setSelectedTab(tab);
|
||||||
setIsExamplesDropdownOpen(false);
|
setIsExamplesDropdownOpen(false);
|
||||||
|
setImportError(null);
|
||||||
if (tab === TabType.Blank) {
|
if (tab === TabType.Blank) {
|
||||||
setCustomPrompt('');
|
setCustomPrompt('');
|
||||||
} else if (tab === TabType.Describe) {
|
} else if (tab === TabType.Describe) {
|
||||||
setCustomPrompt('');
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
|
|
@ -265,7 +313,7 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
|
||||||
|
|
||||||
<form
|
<form
|
||||||
id="create-project-form"
|
id="create-project-form"
|
||||||
action={handleSubmit}
|
action={selectedTab !== TabType.Import ? handleSubmit : undefined}
|
||||||
className="pt-6 pb-16 space-y-12"
|
className="pt-6 pb-16 space-y-12"
|
||||||
>
|
>
|
||||||
{/* Tab Section */}
|
{/* Tab Section */}
|
||||||
|
|
@ -341,9 +389,61 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
variant={selectedTab === TabType.Import ? 'primary' : 'tertiary'}
|
||||||
|
size="md"
|
||||||
|
onClick={() => handleTabChange(TabType.Import)}
|
||||||
|
type="button"
|
||||||
|
className={selectedTab === TabType.Import ? selectedTabStyles : unselectedTabStyles}
|
||||||
|
>
|
||||||
|
Import JSON
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Import JSON Section */}
|
||||||
|
{selectedTab === TabType.Import && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<label className="text-base font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
🗂️ Paste JSON Contents
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
value={importJson}
|
||||||
|
onChange={e => setImportJson(e.target.value)}
|
||||||
|
placeholder="Paste your workflow JSON here..."
|
||||||
|
className={clsx(
|
||||||
|
textareaStyles,
|
||||||
|
"text-base",
|
||||||
|
"text-gray-900 dark:text-gray-100",
|
||||||
|
!importJson && emptyTextareaStyles
|
||||||
|
)}
|
||||||
|
style={{ minHeight: "180px" }}
|
||||||
|
autoFocus
|
||||||
|
autoResize
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col items-start gap-2">
|
||||||
|
{importLoading && (
|
||||||
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Please hold on while we set up your project…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
onClick={handleImportSubmit}
|
||||||
|
type="button"
|
||||||
|
isLoading={importLoading}
|
||||||
|
startContent={<PlusIcon size={16} />}
|
||||||
|
>
|
||||||
|
Import and create assistant
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Custom Prompt Section - Only show when needed */}
|
{/* Custom Prompt Section - Only show when needed */}
|
||||||
{(selectedTab === TabType.Describe || selectedTab === TabType.Example) && (
|
{(selectedTab === TabType.Describe || selectedTab === TabType.Example) && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
@ -400,7 +500,7 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Name Section */}
|
{/* Name Section */}
|
||||||
{USE_MULTIPLE_PROJECTS && (
|
{USE_MULTIPLE_PROJECTS && selectedTab !== TabType.Import && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<label className={largeSectionHeaderStyles}>
|
<label className={largeSectionHeaderStyles}>
|
||||||
|
|
@ -424,9 +524,11 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<div className="pt-1 w-full -mt-4">
|
{selectedTab !== TabType.Import && (
|
||||||
<Submit />
|
<div className="pt-1 w-full -mt-4">
|
||||||
</div>
|
<Submit />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -435,6 +537,15 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
|
||||||
onClose={() => setBillingError(null)}
|
onClose={() => setBillingError(null)}
|
||||||
errorMessage={billingError || ''}
|
errorMessage={billingError || ''}
|
||||||
/>
|
/>
|
||||||
|
<Modal
|
||||||
|
isOpen={importModalOpen}
|
||||||
|
onClose={() => setImportModalOpen(false)}
|
||||||
|
title="Import Error"
|
||||||
|
>
|
||||||
|
<div className="text-red-500 text-sm whitespace-pre-wrap">
|
||||||
|
{importError}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue