diff --git a/apps/docs/actions/open-in-chat.ts b/apps/docs/actions/open-in-chat.ts index 377ec6c3c6..3032069c8b 100644 --- a/apps/docs/actions/open-in-chat.ts +++ b/apps/docs/actions/open-in-chat.ts @@ -1,25 +1,23 @@ "use server"; -import {SandpackFiles} from "@codesandbox/sandpack-react/types"; - -import {parseDependencies} from "@/components/docs/components/code-demo/parse-dependencies"; +import {toKebabCase, toPascalCase} from "@/components/docs/components/code-demo/utils"; const importReact = 'import React from "react";'; -export const openInChat = async ({title, files}: {title?: string; files: SandpackFiles}) => { +export const openInChat = async ({ + component, + title, + content, + dependencies, + useWrapper, +}: { + component: string; + title?: string; + content: string; + dependencies: {name: string; version: string}[]; + useWrapper: boolean; +}) => { try { - // assumes one file for now - let content = files["/App.jsx"]; - - if (!content || typeof content !== "string") { - return { - error: "Content is not a string", - data: null, - }; - } - - const dependencies = parseDependencies(content); - // Check if the file content includes 'React' import statements, if not, add it if ( content.includes("React.") && @@ -29,6 +27,16 @@ export const openInChat = async ({title, files}: {title?: string; files: Sandpac content = `${importReact}\n${content}\n`; } + let files: Record = { + "src/App.tsx": content, + }; + + const fullName = `${component.charAt(0).toUpperCase() + component.slice(1)} - ${title}`; + + if (useWrapper) { + files = getFilesWithWrapper(fullName, content); + } + const response = await fetch(`${process.env.CHAT_API_URL}/import`, { method: "POST", headers: { @@ -36,8 +44,8 @@ export const openInChat = async ({title, files}: {title?: string; files: Sandpac Authorization: `Bearer ${process.env.IMPORT_API_KEY}`, }, body: JSON.stringify({ - title, - content, + title: `${component.charAt(0).toUpperCase() + component.slice(1)} - ${title}`, + files, dependencies, }), }); @@ -63,3 +71,30 @@ export const openInChat = async ({title, files}: {title?: string; files: Sandpac return {error: error, data: null}; } }; + +const getFilesWithWrapper = (name: string, content: string) => { + const pascalName = toPascalCase(name); + const kebabName = toKebabCase(name); + + // Replace the export default function name + const updatedContent = content.replace( + "export default function App()", + `export default function ${pascalName}()`, + ); + + const wrapperContent = `import ${pascalName} from "./components/${kebabName}"; + +export default function App() { + return ( +
+ <${pascalName} /> +
+ ); +} +`; + + return { + [`src/components/${kebabName}.tsx`]: updatedContent, + [`src/App.tsx`]: wrapperContent, + }; +}; diff --git a/apps/docs/components/docs/components/code-demo/code-demo.tsx b/apps/docs/components/docs/components/code-demo/code-demo.tsx index c4ef5235ce..f8e166e5a1 100644 --- a/apps/docs/components/docs/components/code-demo/code-demo.tsx +++ b/apps/docs/components/docs/components/code-demo/code-demo.tsx @@ -9,6 +9,7 @@ import {usePathname} from "next/navigation"; import {useCodeDemo, UseCodeDemoProps} from "./use-code-demo"; import WindowResizer, {WindowResizerProps} from "./window-resizer"; +import {parseDependencies} from "./parse-dependencies"; import {GradientBoxProps} from "@/components/gradient-box"; import {SmallLogo} from "@/components/heroui-logo"; @@ -180,15 +181,34 @@ export const CodeDemo: React.FC = ({ const handleOpenInChat = useCallback(async () => { setIsLoading(true); + // assume doc demo files are all App.jsx + const content = files["/App.jsx"]; + + if (!content || typeof content !== "string") { + addToast({ + title: "Error", + description: "Invalid demo content", + color: "danger", + }); + + return; + } + const component = pathname.split("/components/")[1]; + const dependencies = parseDependencies(content); posthog.capture("CodeDemo - Open in Chat", { component, demo: title, }); - const capitalizedPath = component.charAt(0).toUpperCase() + component.slice(1); - const {data, error} = await openInChat({title: `${capitalizedPath} - ${title}`, files}); + const {data, error} = await openInChat({ + component, + title, + content, + dependencies, + useWrapper: !asIframe, + }); setIsLoading(false); diff --git a/apps/docs/components/docs/components/code-demo/parse-dependencies.ts b/apps/docs/components/docs/components/code-demo/parse-dependencies.ts index cfcb10b5a3..4f8f921fa5 100644 --- a/apps/docs/components/docs/components/code-demo/parse-dependencies.ts +++ b/apps/docs/components/docs/components/code-demo/parse-dependencies.ts @@ -1,15 +1,39 @@ -const packageRegex = /(?:from|import)\s+(?:.*\s+from\s+)?['"]([^'"]+)['"]/g; +import React from "react"; +import * as HeroUI from "@heroui/react"; + +const importRegex = /^(import\s+(?!type\s+\{)[\s\S]*?;)/gm; export const parseDependencies = (content: string) => { const dependencies: {name: string; version: string}[] = []; - content.match(packageRegex)?.forEach((match) => { - if (match.includes("@heroui")) { - return; + // by default, react and heroui packages are installed already + const installedPackages = { + React, + ...HeroUI, + } as Record; + + // create a map of installed packages + const imports = Object.keys(installedPackages).reduce( + (acc, key) => { + acc[key] = `${key}`; + + return acc; + }, + {React: "React"} as Record, + ); + + // match all imports from the file content + content.match(importRegex)?.forEach((match) => { + // check if imported component is in default installed packages + const componentName = match.match(/\w+/g)?.[1] || ""; + const matchingImport = imports[componentName]; + + if (matchingImport) { + return ""; } if (match.includes("./") || match.includes("../")) { - return; + return ""; } const packageName = match.match(/['"]([^'"]+)['"]/)?.[1]; diff --git a/apps/docs/components/docs/components/code-demo/utils.ts b/apps/docs/components/docs/components/code-demo/utils.ts index 754ef44ca1..bab64fd7d0 100644 --- a/apps/docs/components/docs/components/code-demo/utils.ts +++ b/apps/docs/components/docs/components/code-demo/utils.ts @@ -55,3 +55,20 @@ export const joinCode = (filesCode: FileCode[]) => { export const getFileName = (filePath: string) => { return filePath?.split(".")?.[0]?.replace(/\W/g, ""); }; + +export const toPascalCase = (str: string) => { + const cleanStr = str.replace(/[^a-zA-Z0-9\s]/g, ""); + + return cleanStr + .split(/\s+/) + .map((word) => { + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + }) + .join(""); +}; + +export const toKebabCase = (str: string) => { + const cleanStr = str.replace(/[^a-zA-Z0-9\s]/g, ""); + + return cleanStr.toLowerCase().split(/\s+/).join("-"); +};