diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09f8a58c4..dab446d8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,9 +25,9 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v3 with: - node-version: 18.14.0 + node-version: 18.17.0 cache: 'yarn' - + - name: Creating env file run: cp .env.example .env.local diff --git a/.vscode/settings.json b/.vscode/settings.json index f1d06345c..113a9d797 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,21 +3,6 @@ "git.ignoreLimitWarning": true, "deno.enable": true, "deno.unstable": true, - "material-icon-theme.folders.associations": { - "zipper.dev": "Client", - "blog": "Public", - "zipper.run": "Container", - "@zipper-ui": "mjml", - "@zipper-framework": "core", - "@zipper-types": "typescript", - "@zipper-utils": "Utils", - "deno-types": "Lib", - "tsconfig": "Less", - "connectors": "Connection" - }, - "material-icon-theme.files.associations": { - ".ctirc": "verilog" - }, "deno.enablePaths": [ "./packages/@zipper-framework/bin", "./packages/@zipper-framework/deno", diff --git a/apps/zipper.dev/next-env.d.ts b/apps/zipper.dev/next-env.d.ts index 4f11a03dc..fd36f9494 100644 --- a/apps/zipper.dev/next-env.d.ts +++ b/apps/zipper.dev/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/zipper.dev/next.config.js b/apps/zipper.dev/next.config.js index d453a6319..d180d4aa5 100644 --- a/apps/zipper.dev/next.config.js +++ b/apps/zipper.dev/next.config.js @@ -66,8 +66,10 @@ module.exports = getConfig({ '@zipper/types', '@zipper/ui', '@zipper/utils', + '@zipper/tw', 'monaco-languageclient', '@nivo/line', + 'ts-morph', ], images: { dangerouslyAllowSVG: true, @@ -95,6 +97,10 @@ module.exports = getConfig({ config.module.noParse.push( require.resolve('@ts-morph/common/dist/typescript.js'), ); + config.module = { + ...config.module, + exprContextCritical: false, + }; return config; }, // old url: https://similar-years-645746.framer.app (for rollbacks if necessary) diff --git a/apps/zipper.dev/package.json b/apps/zipper.dev/package.json index ec478f8de..1bd6ffdab 100644 --- a/apps/zipper.dev/package.json +++ b/apps/zipper.dev/package.json @@ -3,6 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { + "next:dev": "next dev", "build:01-prisma": "prisma generate", "build:02-next": "next build", "build:03-tsc": "tsc --project tsconfig.server.json", @@ -11,7 +12,7 @@ "build:production": "yarn build:01-prisma; yarn dotenv:production -- -- next build --no-lint; yarn build:03-tsc", "db-seed": "prisma db seed", "db-migrate-dev": "yarn prisma migrate dev", - "dev": "yarn dotenv:development -- -- yarn nodemon -x \"yarn swc-node ./src/server.ts || touch ./src/server.ts\" --watch ./src/server.ts", + "dev": "next dev", "dx": "run-s db-up db-migrate-dev db-seed dev", "dotenv:development": "dotenv -c $(sh ../../bin/dotenv.development.sh)", "dotenv:preview": "dotenv $(sh ../../bin/dotenv.preview.sh)", @@ -53,6 +54,9 @@ "@hookform/resolvers": "^3.3.1", "@june-so/analytics-next": "^2.0.0", "@june-so/analytics-node": "^8.0.0", + "@langchain/community": "^0.0.17", + "@langchain/core": "^0.1.12", + "@langchain/openai": "^0.0.11", "@liveblocks/client": "^1.9.6", "@liveblocks/node": "^1.9.6", "@liveblocks/react": "^1.9.6", @@ -62,16 +66,18 @@ "@nicksrandall/console-feed": "^3.5.0", "@nivo/bar": "^0.84.0", "@nivo/line": "^0.84.0", + "@pinecone-database/pinecone": "^1.1.2", "@prisma/client": "^5.4.2", "@radix-ui/react-icons": "^1.3.0", - "@react-email/components": "^0.0.7", - "@sentry/nextjs": "^7.51.0", + "@react-email/components": "^0.0.11", + "@react-email/render": "0.0.9-canary.2", + "@sentry/nextjs": "^7.72.0", "@tanstack/react-query": "^4.35.3", "@tanstack/react-table": "^8.7.9", - "@trpc/client": "^10.38.4", - "@trpc/next": "^10.38.4", - "@trpc/react-query": "^10.38.4", - "@trpc/server": "^10.38.4", + "@trpc/client": "^10.43.6", + "@trpc/next": "^10.43.6", + "@trpc/react-query": "^10.43.6", + "@trpc/server": "^10.43.6", "@types/into-stream": "^3.1.1", "@types/ndjson": "^2.0.1", "@types/node": "^18.7.16", @@ -83,6 +89,7 @@ "@zipper-inc/client-js": "^0.1.6", "@zipper/types": "*", "@zipper/ui": "*", + "@zipper/tw": "*", "@zipper/utils": "*", "ai": "^2.1.14", "autoprefixer": "^10.4.16", @@ -103,7 +110,7 @@ "jose": "^4.11.4", "jsonwebtoken": "^9.0.0", "jszip": "^3.10.1", - "langchain": "^0.0.130", + "langchain": "^0.1.2", "lodash.debounce": "^4.0.8", "lodash.isequal": "^4.5.0", "lodash.shuffle": "^4.2.0", @@ -136,7 +143,8 @@ "react-hook-form": "^7.49.3", "react-icons": "^4.10.1", "react-timeago": "^7.2.0", - "resend": "^0.15.3", + "resend": "^2.1.0", + "server-only": "^0.0.1", "slugify": "^1.6.5", "sooner": "^1.1.4", "ssrfcheck": "^1.0.2", @@ -196,5 +204,11 @@ }, "publishConfig": { "access": "restricted" + }, + "resolutions": { + "@langchain/core": "0.1.12" + }, + "overrides": { + "@langchain/core": "0.1.12" } } diff --git a/apps/zipper.dev/src/app/[resource-owner]/create/page.tsx b/apps/zipper.dev/src/app/[resource-owner]/create/page.tsx new file mode 100644 index 000000000..bc8aaba81 --- /dev/null +++ b/apps/zipper.dev/src/app/[resource-owner]/create/page.tsx @@ -0,0 +1,489 @@ +'use client'; +import Link from 'next/link'; +import { useOrganization } from '~/hooks/use-organization'; +import { PiArrowLeftLight, PiCode } from 'react-icons/pi'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { cn } from '@zipper/tw/cn'; +import { + Tabs, + Divider, + Label, + Input, + Form, + Switch, + Button, + List, + Textarea, + Show, +} from '@zipper/tw/ui'; +import { generateDefaultSlug } from '~/utils/generate-default'; +import { useForm } from 'react-hook-form'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { z } from 'zod'; +import { HiLockClosed, HiLockOpen } from 'react-icons/hi'; +import { trpc } from '~/utils/trpc'; +import { MIN_SLUG_LENGTH, useAppSlug } from '~/hooks/use-app-slug'; +import { HiExclamationTriangle } from 'react-icons/hi2'; +import { CheckIcon } from '@radix-ui/react-icons'; +import { useOrganizationList } from '~/hooks/use-organization-list'; +import { useRouter } from 'next/navigation'; +import { getEditAppletLink } from '@zipper/utils'; +// import { useEditorContext } from '~/components/context/editor-context'; + +const getDefaultCreateAppFormValues = () => ({ + name: generateDefaultSlug(), + description: '', + isPublic: false, + requiresAuthToRun: false, +}); + +const DEFAULT_TEMPLATES = [ + { + id: 'hello-world', + name: 'Hello World', + description: '👋', + shouldFork: false, + }, + { + id: 'ai', + name: 'AI Generated Code', + description: '🤖✨', + shouldFork: false, + }, +]; + +const CreateFormSchema = z.object({ + name: z.string().min(2, { + message: 'applet-name must be at least 2 characters.', + }), + description: z.string().optional(), + isPublic: z.boolean().default(false), + requiresAuthToRun: z.boolean().default(false), +}); + +const DashboardCreateZiplet = ({ params }: { params: { slug: string } }) => { + /* ------------------- Hooks ------------------ */ + const createAppForm = useForm>({ + resolver: zodResolver(CreateFormSchema as any), + defaultValues: getDefaultCreateAppFormValues(), + }); + + const { organization } = useOrganization(); + const { setActive } = useOrganizationList(); + const router = useRouter(); + const utils = trpc.useUtils(); + // const { scripts } = useEditorContext(); + + const { slugExists, appSlugQuery } = useAppSlug(createAppForm.watch('name')); + + /* ------------------ States ------------------ */ + const [currentTab, setCurrentTab] = useState('first-step'); + const [currentSelectedTemplate, setCurrentSelectedTemplate] = useState< + (typeof DEFAULT_TEMPLATES)[number] | undefined + >(undefined); + const [templates, setTemplates] = useState< + (typeof DEFAULT_TEMPLATES)[number][] + >([]); + + /* ------------------ Queries ----------------- */ + const templatesQuery = trpc.app.templates.useQuery(); + + /* ----------------- Mutation ----------------- */ + const addScript = trpc.script.add.useMutation(); + const generateCodeWithAI = trpc.ai.pipeline.useMutation(); + + const addApp = trpc.app.add.useMutation({ + async onSuccess() { + // refetches posts after a post is added + await utils.app.byAuthedUser.invalidate(); + if (params.slug) { + await utils.app.byResourceOwner.invalidate({ + resourceOwnerSlug: params.slug as string, + }); + } + }, + }); + + const forkTemplate = trpc.app.fork.useMutation({ + async onSuccess() { + // refetches posts after a post is added + await utils.app.byAuthedUser.invalidate(); + if (params.slug) { + await utils.app.byResourceOwner.invalidate({ + resourceOwnerSlug: params.slug as string, + }); + } + }, + }); + + /* ----------------- Callbacks ---------------- */ + const runAddAppMutation = useCallback(async () => { + const { name, description, isPublic, requiresAuthToRun } = { + name: createAppForm.getValues('name'), + description: createAppForm.getValues('description'), + isPublic: createAppForm.getValues('isPublic'), + requiresAuthToRun: createAppForm.getValues('requiresAuthToRun'), + }; + + const aiOutput = + currentSelectedTemplate?.name === 'AI Generated Code' && description + ? await generateCodeWithAI.mutateAsync({ + userRequest: description, + }) + : undefined; + + const mainCode = aiOutput?.find( + (output) => output.filename === 'main.ts', + )?.code; + + await addApp.mutateAsync( + { + description, + name, + isPrivate: !isPublic, + requiresAuthToRun, + organizationId: organization?.id, + aiCode: mainCode, + }, + { + onSuccess: async (applet) => { + createAppForm.reset(getDefaultCreateAppFormValues()); + + if ( + (organization?.id ?? null) !== (organization?.id ?? null) && + setActive + ) { + await setActive(organization?.id || null); + } + if (aiOutput) { + const otherFiles = aiOutput.filter( + (output) => + output.filename !== 'main.ts' && output.filename !== 'main.tsx', + ); + + await Promise.allSettled( + otherFiles.map((output) => { + return addScript.mutateAsync({ + filename: output.filename, + appId: applet!.id, + order: otherFiles.length + 1, + code: output.code, + }); + }), + ); + } + + // toast('Ziplet has been created', { + // description: 'Redirecting you to playground...', + // duration: 9999, + // }); + + router.push( + getEditAppletLink(applet!.resourceOwner!.slug, applet!.slug), + ); + }, + }, + ); + }, []); + + const onSubmit = useCallback(async (data: any) => { + const selectedTemplate = templates.find( + (t) => t.id === currentSelectedTemplate?.id, + ); + + if (selectedTemplate?.shouldFork) { + await forkTemplate.mutateAsync( + { + id: selectedTemplate.id, + name: data.name, + connectToParent: false, + }, + { + onSuccess: (applet) => { + createAppForm.reset(); + + /** TO-DO: verify toaster in app dir */ + + // toast('Ziplet has been created', { + // description: 'Redirecting you to playground...', + // duration: 9999, + // }); + + router.push( + getEditAppletLink(applet!.resourceOwner!.slug, applet!.slug), + ); + }, + }, + ); + } else { + await runAddAppMutation(); + } + }, []); + + /* ------------------- Memos ------------------ */ + const isSlugValid = useMemo(() => { + const slug = createAppForm.watch('name'); + console.log('query', appSlugQuery.isFetched); + console.log( + 'slug', + appSlugQuery.isFetched && slug.length >= MIN_SLUG_LENGTH, + ); + return appSlugQuery.isFetched && slug.length >= MIN_SLUG_LENGTH; + }, [appSlugQuery.isFetched, createAppForm.watch]); + + const isSubmitDisabled = useMemo( + () => + !currentSelectedTemplate || + (currentSelectedTemplate.name === 'AI Generated Code' && + !createAppForm.getValues().description) || + slugExists || + createAppForm.getValues().name.length < MIN_SLUG_LENGTH || + addApp.isLoading || + generateCodeWithAI.isLoading || + createAppForm.formState.isSubmitting, + [ + createAppForm, + currentSelectedTemplate, + addApp.isLoading, + generateCodeWithAI, + ], + ); + + /* ------------------ Effects ----------------- */ + useEffect(() => { + if (templatesQuery.data) { + const mappedQueryTemplates = templatesQuery.data.map((template) => { + return { + id: template.id, + name: template.name, + description: template.description, + shouldFork: true, + } as (typeof DEFAULT_TEMPLATES)[number]; + }); + + setTemplates([...DEFAULT_TEMPLATES, ...mappedQueryTemplates]); + } + }, [templatesQuery.data]); + + return ( +
+
+ + + Back to Dashboard + + + + + setCurrentTab('first-step')} + className="bg-background group flex items-center gap-3 px-0 data-[state=active]:shadow-none" + > + + 1 + +
+ First +

Configuration

+
+
+ + setCurrentTab('second-step')} + className="bg-background group flex items-center gap-3 px-0 data-[state=active]:shadow-none" + > + + 2 + +
+ Second +

Initialization

+
+
+
+
+ + +
+ ( + + + Applet Name + +
+ + {organization?.name} + + / + + + + + } + > + + +
+ {`Your app will be available at https://${field.value}.zipper.run`} +
+ )} + >
+
+ + + +
+ + ( + + + + Is this code public? + + + + + + )} + /> + + ( + + + {field.value === true ? ( + + ) : ( + + )} + Require sign in to run? + + + + + + )} + /> +
+ +
+ + +
+
+ + + Start from a Template +
+ + {(template) => ( +
+ setCurrentSelectedTemplate((prev) => + prev?.id === template.id ? undefined : template, + ) + } + className={cn( + 'h-32 hover:-translate-y-1 cursor-pointer border border-background hover:border-muted transition-all flex flex-col justify-center p-6 shadow bg-background', + currentSelectedTemplate?.id === template.id && + 'border-primary hover:border-primary', + )} + > +

{template.name}

+

{template.description}

+
+ )} +
+
+
+ + + ( + + Generate code using AI + + Tell us what you'd like your applet to do and we'll + use the magic of AI to autogenerate some code to get + you started.{' '} + This process can take up to a minute. + + +