diff --git a/app/(playground)/p/[agentId]/canary/actions.ts b/app/(playground)/p/[agentId]/canary/actions.ts index e82ad073..9a433caf 100644 --- a/app/(playground)/p/[agentId]/canary/actions.ts +++ b/app/(playground)/p/[agentId]/canary/actions.ts @@ -23,6 +23,7 @@ import { vercelBlobFileFolder, vercelBlobGraphFolder } from "./constants"; import { textGenerationPrompt } from "./lib/prompts"; import { + buildFileFolderPath, buildGraphPath, elementsToMarkdown, langfuseModel, @@ -418,7 +419,7 @@ export async function putGraph(graph: Graph) { export async function remove(fileData: FileData) { const blobList = await list({ - prefix: pathJoin(vercelBlobFileFolder, fileData.id), + prefix: buildFileFolderPath(fileData.id), }); if (blobList.blobs.length > 0) { diff --git a/app/(playground)/p/[agentId]/canary/lib/utils.ts b/app/(playground)/p/[agentId]/canary/lib/utils.ts index 23ba7c7c..7271dd24 100644 --- a/app/(playground)/p/[agentId]/canary/lib/utils.ts +++ b/app/(playground)/p/[agentId]/canary/lib/utils.ts @@ -1,5 +1,5 @@ import { createId } from "@paralleldrive/cuid2"; -import { vercelBlobGraphFolder } from "../constants"; +import { vercelBlobFileFolder, vercelBlobGraphFolder } from "../constants"; import type { ArtifactId, ConnectionId, @@ -180,6 +180,9 @@ export function initGraph(): Graph { }; } +export function buildFileFolderPath(fileId: FileId) { + return pathJoin(vercelBlobFileFolder, fileId); +} export function buildGraphFolderPath(graphId: GraphId) { return pathJoin(vercelBlobGraphFolder, graphId); } diff --git a/app/dev/copy-agent/action.ts b/app/dev/copy-agent/action.ts new file mode 100644 index 00000000..1f823f0f --- /dev/null +++ b/app/dev/copy-agent/action.ts @@ -0,0 +1,122 @@ +"use server"; + +import { agents, db } from "@/drizzle"; +import { fetchCurrentUser } from "@/services/accounts"; +import { fetchCurrentTeam } from "@/services/teams"; +import { createId } from "@paralleldrive/cuid2"; +import { copy, list } from "@vercel/blob"; +import { putGraph } from "../../(playground)/p/[agentId]/canary/actions"; +import { + buildFileFolderPath, + createFileId, + pathJoin, +} from "../../(playground)/p/[agentId]/canary/lib/utils"; +import type { + AgentId, + Graph, + Node, +} from "../../(playground)/p/[agentId]/canary/types"; + +interface AgentDuplicationSuccess { + result: "success"; + agentId: AgentId; +} +interface AgentDuplicationError { + result: "error"; + message: string; +} +type AgentDuplicationResult = AgentDuplicationSuccess | AgentDuplicationError; + +export async function copyAgentAction( + prev: AgentDuplicationResult | null, + formData: FormData, +): Promise { + const agentId = formData.get("agentId"); + if (typeof agentId !== "string" || agentId.length === 0) { + return { result: "error", message: "Please fill in the agent id" }; + } + const agent = await db.query.agents.findFirst({ + where: (agents, { eq }) => eq(agents.id, agentId as AgentId), + }); + if (agent === undefined || agent.graphUrl === null) { + return { result: "error", message: `${agentId} is not found.` }; + } + const [user, team, graph] = await Promise.all([ + fetchCurrentUser(), + fetchCurrentTeam(), + fetch(agent.graphUrl).then((res) => res.json() as unknown as Graph), + ]); + const newNodes = await Promise.all( + graph.nodes.map(async (node) => { + if (node.content.type !== "files") { + return node; + } + const newData = await Promise.all( + node.content.data.map(async (fileData) => { + if (fileData.status !== "completed") { + return null; + } + const newFileId = createFileId(); + const blobList = await list({ + prefix: buildFileFolderPath(fileData.id), + }); + let newFileBlobUrl = ""; + let newTextDataUrl = ""; + await Promise.all( + blobList.blobs.map(async (blob) => { + const copyResult = await copy( + blob.url, + buildFileFolderPath(newFileId), + { + access: "public", + }, + ); + if (blob.url === fileData.fileBlobUrl) { + newFileBlobUrl = copyResult.url; + } + if (blob.url === fileData.textDataUrl) { + newTextDataUrl = copyResult.url; + } + }), + ); + return { + ...fileData, + id: newFileId, + fileBlobUrl: newFileBlobUrl, + textDataUrl: newTextDataUrl, + }; + }), + ).then((data) => data.filter((d) => d !== null)); + return { + ...node, + content: { + ...node.content, + data: newData, + }, + } as Node; + }), + ); + const { url } = await putGraph({ ...graph, nodes: newNodes }); + const newAgentId = `agnt_${createId()}` as AgentId; + const newAgent = await db.insert(agents).values({ + id: newAgentId, + name: `Copy of ${agent.name ?? agentId}`, + teamDbId: team.dbId, + creatorDbId: user.dbId, + graphUrl: url, + graphv2: { + agentId: newAgentId, + nodes: [], + xyFlow: { + nodes: [], + edges: [], + }, + connectors: [], + artifacts: [], + webSearches: [], + mode: "edit", + flowIndexes: [], + }, + }); + return { result: "success", agentId: newAgentId }; +} diff --git a/app/dev/copy-agent/agent-id-form.tsx b/app/dev/copy-agent/agent-id-form.tsx new file mode 100644 index 00000000..c3e62496 --- /dev/null +++ b/app/dev/copy-agent/agent-id-form.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { CheckIcon, CopyCheckIcon } from "lucide-react"; +import Link from "next/link"; +import { useActionState } from "react"; +import { Spinner } from "../tabui/components/spinner"; +import { copyAgentAction } from "./action"; + +export default function AgentIdForm() { + const [state, formAction, isPending] = useActionState(copyAgentAction, null); + return ( +
+
+
+ {">"} + { + ref?.focus(); + function submitAgentId() { + if (/agnt_[a-z0-9]{24}/.test(ref?.value ?? "")) { + ref?.form?.requestSubmit(); + } + } + ref?.addEventListener("input", submitAgentId); + return () => { + ref?.removeEventListener("input", submitAgentId); + }; + }} + /> +
+ {isPending && } + {!isPending && state?.result === "error" ? ( +

{state.message}

+ ) : state?.result === "success" ? ( +
+
+

Success

+
+ + Open in a new tab + +
+ ) : null} +
+
+ ); +} diff --git a/app/dev/copy-agent/page.tsx b/app/dev/copy-agent/page.tsx new file mode 100644 index 00000000..dc36baad --- /dev/null +++ b/app/dev/copy-agent/page.tsx @@ -0,0 +1,29 @@ +import { agents, db } from "@/drizzle"; +import { developerFlag } from "@/flags"; +import { fetchCurrentUser } from "@/services/accounts"; +import { fetchCurrentTeam } from "@/services/teams"; +import { createId } from "@paralleldrive/cuid2"; +import { notFound } from "next/navigation"; +import { putGraph } from "../../(playground)/p/[agentId]/canary/actions"; +import type { + AgentId, + Graph, +} from "../../(playground)/p/[agentId]/canary/types"; +import AgentIdForm from "./agent-id-form"; + +export default async function CopyAgentPage() { + const developerMode = await developerFlag(); + if (!developerMode) { + return notFound(); + } + + return ( +
+

+ Please fill in the agent id, then copy it in your + account.(agnt_[0-9a-zA-Z]+) +

+ +
+ ); +}