Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/docs/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 58 additions & 0 deletions apps/docs/actions/open-in-chat.ts
Original file line number Diff line number Diff line change
@@ -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,
}),
});
Comment on lines +32 to +43
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for API request

The fetch request lacks timeout and network error handling.

-    const response = await fetch(`${process.env.CHAT_API_URL}/import`, {
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 10000); // 10-second timeout
+
+    const response = await fetch(`${process.env.CHAT_API_URL}/import`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.IMPORT_API_KEY}`,
      },
+     signal: controller.signal,
      body: JSON.stringify({
        title,
        content,
        dependencies,
      }),
    });
+
+    clearTimeout(timeoutId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10-second timeout
const response = await fetch(`${process.env.CHAT_API_URL}/import`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.IMPORT_API_KEY}`,
},
signal: controller.signal,
body: JSON.stringify({
title,
content,
dependencies,
}),
});
clearTimeout(timeoutId);


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};
}
Comment on lines +55 to +57
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling in catch block

The error message may not be a string, which could cause issues when returned.

-    return {error: error, data: null};
+    const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
+    console.error("Error in openInChat:", error);
+    return {error: errorMessage, data: null};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
return {error: error, data: null};
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
console.error("Error in openInChat:", error);
return {error: errorMessage, data: null};
}

};
4 changes: 3 additions & 1 deletion apps/docs/app/docs/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 />}
</>
);
}
104 changes: 86 additions & 18 deletions apps/docs/components/docs/components/code-demo/code-demo.tsx
Original file line number Diff line number Diff line change
@@ -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),
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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