Skip to content

Commit

Permalink
feat: Mardown support (experimental) (#247)
Browse files Browse the repository at this point in the history
* feat: Mardown support (experimental)

* do not publish react-markdown yet

* fix imports
  • Loading branch information
Yonom authored Jun 19, 2024
1 parent fd9c38a commit 7c3f6aa
Show file tree
Hide file tree
Showing 17 changed files with 1,006 additions and 1 deletion.
1 change: 1 addition & 0 deletions apps/www/components/ui/assistant-ui/codeblock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "assistant-ui/registry/assistant-ui/experimental/codeblock";
1 change: 1 addition & 0 deletions apps/www/components/ui/assistant-ui/markdown-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "assistant-ui/registry/assistant-ui/experimental/markdown-text";
2 changes: 1 addition & 1 deletion apps/www/tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const config = {
},
},
},
plugins: [require("tailwindcss-animate")],
plugins: [require("@tailwindcss/typography"), require("tailwindcss-animate")],
} satisfies Config;

export default config;
1 change: 1 addition & 0 deletions packages/cli/components/ui/assistant-ui/codeblock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "@/registry/assistant-ui/experimental/codeblock";
1 change: 1 addition & 0 deletions packages/cli/components/ui/assistant-ui/markdown-text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "@/registry/assistant-ui/experimental/markdown-text";
5 changes: 5 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
},
"devDependencies": {
"@assistant-ui/react": "workspace:*",
"@assistant-ui/react-markdown": "workspace:*",
"@assistant-ui/tsconfig": "workspace:*",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
Expand All @@ -21,11 +22,15 @@
"@types/cross-spawn": "^6.0.6",
"@types/node": "^20.14.2",
"@types/react": "^18",
"@types/react-syntax-highlighter": "^15.5.13",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.395.0",
"react": "^18.3.1",
"react-resizable-panels": "^2.0.19",
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"rimraf": "^5.0.7",
"shadcn-ui": "0.8.0",
"tailwind-merge": "^2.3.0",
Expand Down
181 changes: 181 additions & 0 deletions packages/cli/registry/assistant-ui/experimental/codeblock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Inspired by Chatbot-UI and modified to fit the needs of this project
// @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Markdown/CodeBlock.tsx

"use client";

import { type FC, memo, useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism";

import { Button } from "@/components/ui/button";
import { CheckIcon, CopyIcon, DownloadIcon } from "lucide-react";

interface Props {
language: string;
value: string;
}

interface languageMap {
[key: string]: string | undefined;
}

export const programmingLanguages: languageMap = {
javascript: ".js",
python: ".py",
java: ".java",
c: ".c",
cpp: ".cpp",
"c++": ".cpp",
"c#": ".cs",
ruby: ".rb",
php: ".php",
swift: ".swift",
"objective-c": ".m",
kotlin: ".kt",
typescript: ".ts",
go: ".go",
perl: ".pl",
rust: ".rs",
scala: ".scala",
haskell: ".hs",
lua: ".lua",
shell: ".sh",
sql: ".sql",
html: ".html",
css: ".css",
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
};

export const generateRandomString = (length: number, lowercase = false) => {
const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0
let result = "";
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return lowercase ? result.toLowerCase() : result;
};

const CodeBlock: FC<Props> = memo(({ language, value }) => {
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });

const downloadAsFile = () => {
if (typeof window === "undefined") {
return;
}
const fileExtension = programmingLanguages[language] || ".file";
const suggestedFileName = `file-${generateRandomString(
3,
true,
)}${fileExtension}`;
const fileName = window.prompt("Enter file name" || "", suggestedFileName);

if (!fileName) {
// User pressed cancel on prompt.
return;
}

const blob = new Blob([value], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.download = fileName;
link.href = url;
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};

const onCopy = () => {
if (isCopied) return;
copyToClipboard(value);
};

return (
<div className="codeblock relative w-full bg-zinc-950 font-sans">
<div className="flex w-full items-center justify-between bg-zinc-800 px-6 py-2 pr-4 text-zinc-100">
<span className="text-xs lowercase">{language}</span>
<div className="flex items-center space-x-1">
<Button
variant="ghost"
className="hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
onClick={downloadAsFile}
size="icon"
>
<DownloadIcon className="size-4" />
<span className="sr-only">Download</span>
</Button>
<Button
variant="ghost"
size="icon"
className="text-xs hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
onClick={onCopy}
>
{isCopied ? (
<CheckIcon className="size-4" />
) : (
<CopyIcon className="size-4" />
)}
<span className="sr-only">Copy code</span>
</Button>
</div>
</div>
<SyntaxHighlighter
language={language}
style={coldarkDark}
PreTag="div"
showLineNumbers
customStyle={{
margin: 0,
width: "100%",
background: "transparent",
padding: "1.5rem 1rem",
}}
lineNumberStyle={{
userSelect: "none",
}}
codeTagProps={{
style: {
fontSize: "0.9rem",
fontFamily: "var(--font-mono)",
},
}}
>
{value}
</SyntaxHighlighter>
</div>
);
});
CodeBlock.displayName = "CodeBlock";

export { CodeBlock };

export interface useCopyToClipboardProps {
timeout?: number;
}

export function useCopyToClipboard({
timeout = 2000,
}: useCopyToClipboardProps) {
const [isCopied, setIsCopied] = useState<boolean>(false);

const copyToClipboard = (value: string) => {
if (typeof window === "undefined" || !navigator.clipboard?.writeText) {
return;
}

if (!value) {
return;
}

navigator.clipboard.writeText(value).then(() => {
setIsCopied(true);

setTimeout(() => {
setIsCopied(false);
}, timeout);
});
};

return { isCopied, copyToClipboard };
}
49 changes: 49 additions & 0 deletions packages/cli/registry/assistant-ui/experimental/markdown-text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";

import { unstable_MarkdownTextPrimitive as MarkdownTextPrimitive } from "@assistant-ui/react-markdown";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";

export const MarkdownText = () => {
return (
<MarkdownTextPrimitive
className="prose dark:prose-invert break-words prose-pre:p-0 prose-p:leading-relaxed"
remarkPlugins={[remarkGfm, remarkMath]}
components={{
p({ children }) {
return <p className="mb-2 last:mb-0">{children}</p>;
},
// code({ node, className, children, ...props }) {
// if (children.length) {
// if (children[0] == "▍") {
// return (
// <span className="mt-1 animate-pulse cursor-default">▍</span>
// );
// }

// children[0] = (children[0] as string).replace("`▍`", "▍");
// }

// const match = /language-(\w+)/.exec(className || "");

// if (inline) {
// return (
// <code className={className} {...props}>
// {children}
// </code>
// );
// }

// return (
// <CodeBlock
// key={Math.random()}
// language={match?.[1] || ""}
// value={String(children).replace(/\n$/, "")}
// {...props}
// />
// );
// },
}}
/>
);
};
Loading

0 comments on commit 7c3f6aa

Please sign in to comment.