mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
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:
parent
03ee057eed
commit
80aadd9772
@ -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=
|
||||
|
||||
58
apps/docs/actions/open-in-chat.ts
Normal file
58
apps/docs/actions/open-in-chat.ts
Normal 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};
|
||||
}
|
||||
};
|
||||
@ -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 />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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",
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user