feat: open in chat button in doc examples (#4996)

* feat: open in chat button in doc examples

* feat: pass dependencies in open in chat

* fix: open in chat error handling

* chore: small adjustment

---------

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>
This commit is contained in:
Peterl561 2025-03-10 22:50:50 +08:00 committed by GitHub
parent 03ee057eed
commit 80aadd9772
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 186 additions and 20 deletions

View File

@ -21,4 +21,9 @@ NEXT_PUBLIC_FB_FEEDBACK_URL=
# PostHog
NEXT_PUBLIC_POSTHOG_KEY=your-posthog-key
NEXT_PUBLIC_POSTHOG_HOST=your-posthog-host
NEXT_PUBLIC_POSTHOG_HOST=your-posthog-host
# Chat
IMPORT_API_KEY=your-import-api-key
CHAT_API_URL=
CHAT_URL=

View File

@ -0,0 +1,58 @@
"use server";
import {SandpackFiles} from "@codesandbox/sandpack-react/types";
import {parseDependencies} from "@/components/docs/components/code-demo/parse-dependencies";
const importReact = 'import React from "react";';
export const openInChat = async ({title, files}: {title?: string; files: SandpackFiles}) => {
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,
};
}
// Check if the file content includes 'React' import statements, if not, add it
if (
content.includes("React.") &&
!content.includes("from 'react'") &&
!content.includes('from "react"')
) {
content = `${importReact}\n${content}\n`;
}
const dependencies = parseDependencies(content);
const response = await fetch(`${process.env.CHAT_API_URL}/import`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.IMPORT_API_KEY}`,
},
body: JSON.stringify({
title,
content,
dependencies,
}),
});
const result = await response.json();
if (result.error || !result.path) {
return {
error: result.error ?? "Unknown error",
data: null,
};
}
return {error: null, data: `${process.env.CHAT_URL}${result.path}`};
} catch (error) {
return {error: error, data: null};
}
};

View File

@ -2,7 +2,7 @@ import type {Metadata} from "next";
import {notFound} from "next/navigation";
import {allDocs} from "contentlayer2/generated";
import {Link} from "@heroui/react";
import {Link, ToastProvider} from "@heroui/react";
import {MDXContent} from "@/components/mdx-content";
import {siteConfig} from "@/config/site";
@ -104,6 +104,8 @@ export default async function DocPage({params}: DocPageProps) {
<DocsToc headings={headings} />
</div>
)}
{/* toast page has its own provider*/}
{doc.title !== "Toast" && <ToastProvider />}
</>
);
}

View File

@ -1,14 +1,18 @@
"use client";
import React, {useCallback, useMemo, useRef} from "react";
import React, {useCallback, useMemo, useRef, useState} from "react";
import dynamic from "next/dynamic";
import {Skeleton, Tab, Tabs} from "@heroui/react";
import {addToast, Button, Skeleton, Spinner, Tab, Tabs} from "@heroui/react";
import {useInView} from "framer-motion";
import {usePostHog} from "posthog-js/react";
import {usePathname} from "next/navigation";
import {useCodeDemo, UseCodeDemoProps} from "./use-code-demo";
import WindowResizer, {WindowResizerProps} from "./window-resizer";
import {GradientBoxProps} from "@/components/gradient-box";
import {SmallLogo} from "@/components/heroui-logo";
import {openInChat} from "@/actions/open-in-chat";
const DynamicReactLiveDemo = dynamic(
() => import("./react-live-demo").then((m) => m.ReactLiveDemo),
@ -75,6 +79,11 @@ export const CodeDemo: React.FC<CodeDemoProps> = ({
margin: "600px",
});
const pathname = usePathname();
const posthog = usePostHog();
const [isLoading, setIsLoading] = useState(false);
const {noInline, code} = useCodeDemo({
files,
});
@ -166,24 +175,83 @@ export const CodeDemo: React.FC<CodeDemoProps> = ({
return true;
}, [showTabs, showPreview, showEditor]);
const isComponentsPage = pathname.includes("/components/");
const handleOpenInChat = useCallback(async () => {
setIsLoading(true);
const component = pathname.split("/components/")[1];
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});
setIsLoading(false);
if (error || !data) {
posthog.capture("CodeDemo - Open in Chat Error", {
component,
demo: title,
error: error ?? "Unknown error",
});
addToast({
title: "Error",
description: error ?? "Unknown error",
color: "danger",
});
return;
}
window.open(data, "_blank");
}, [pathname, title, files, posthog]);
return (
<div ref={ref} className="flex flex-col gap-2">
<div ref={ref} className="flex flex-col gap-2 relative">
{shouldRenderTabs ? (
<Tabs
disableAnimation
aria-label="Code demo tabs"
classNames={{
panel: "pt-0",
}}
variant="underlined"
>
<Tab key="preview" title="Preview">
{previewContent}
</Tab>
<Tab key="code" title="Code">
{editorContent}
</Tab>
</Tabs>
<>
<Tabs
disableAnimation
aria-label="Code demo tabs"
classNames={{
panel: "pt-0",
}}
variant="underlined"
>
<Tab key="preview" title="Preview">
{previewContent}
</Tab>
<Tab key="code" title="Code">
{editorContent}
</Tab>
</Tabs>
{isComponentsPage && (
<Button
className="absolute right-1 top-1 border-1"
isDisabled={isLoading}
size="sm"
variant="bordered"
onPress={handleOpenInChat}
>
Open in Chat{" "}
{isLoading ? (
<Spinner
classNames={{wrapper: "h-4 w-4"}}
color="current"
size="sm"
variant="simple"
/>
) : (
<SmallLogo className="w-4 h-4" />
)}
</Button>
)}
</>
) : (
<>
{previewContent}

View File

@ -0,0 +1,33 @@
const packageRegex = /(?:from|import)\s+(?:.*\s+from\s+)?['"]([^'"]+)['"]/g;
export const parseDependencies = (content: string) => {
const dependencies: {name: string; version: string}[] = [];
content.match(packageRegex)?.forEach((match) => {
if (match.includes("@heroui")) {
return;
}
if (match.includes("./") || match.includes("../")) {
return;
}
const packageName = match.match(/['"]([^'"]+)['"]/)?.[1];
if (!packageName) {
return;
}
dependencies.push({
name: packageName,
version: fixedVersions[packageName] || "latest",
});
});
return dependencies;
};
const fixedVersions = {
"@internationalized/date": "3.7.0",
"@react-aria/i18n": "3.12.5",
};