fix(docs): open in chat improvements (#5035)

* fix(docs): parse dependency match imported components against heroui

* fix(docs): center open in chat components
This commit is contained in:
Peterl561 2025-03-12 22:50:35 +08:00 committed by GitHub
parent 0a3773c2a1
commit 26080cbf0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 121 additions and 25 deletions

View File

@ -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<string, string> = {
"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 (
<div className="flex min-h-screen items-center justify-center p-6">
<${pascalName} />
</div>
);
}
`;
return {
[`src/components/${kebabName}.tsx`]: updatedContent,
[`src/App.tsx`]: wrapperContent,
};
};

View File

@ -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<CodeDemoProps> = ({
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);

View File

@ -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<string, unknown>;
// create a map of installed packages
const imports = Object.keys(installedPackages).reduce(
(acc, key) => {
acc[key] = `${key}`;
return acc;
},
{React: "React"} as Record<string, string>,
);
// 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];

View File

@ -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("-");
};