diff --git a/packages/shadcn-registry/components/ui/assistant-ui/codeblock.tsx b/packages/shadcn-registry/components/ui/assistant-ui/codeblock.tsx deleted file mode 100644 index f46cf45cd..000000000 --- a/packages/shadcn-registry/components/ui/assistant-ui/codeblock.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from "@/registry/assistant-ui/experimental/codeblock"; diff --git a/packages/shadcn-registry/components/ui/assistant-ui/syntax-highlighter.tsx b/packages/shadcn-registry/components/ui/assistant-ui/syntax-highlighter.tsx new file mode 100644 index 000000000..df6f0c7f3 --- /dev/null +++ b/packages/shadcn-registry/components/ui/assistant-ui/syntax-highlighter.tsx @@ -0,0 +1 @@ +export * from "@/registry/assistant-ui/syntax-highlighter"; diff --git a/packages/shadcn-registry/components/ui/assistant-ui/tooltip-icon-button.tsx b/packages/shadcn-registry/components/ui/assistant-ui/tooltip-icon-button.tsx new file mode 100644 index 000000000..28175959d --- /dev/null +++ b/packages/shadcn-registry/components/ui/assistant-ui/tooltip-icon-button.tsx @@ -0,0 +1 @@ +export * from "@/registry/assistant-ui/tooltip-icon-button"; diff --git a/packages/shadcn-registry/package.json b/packages/shadcn-registry/package.json index bdfa83e11..3aa0b1363 100644 --- a/packages/shadcn-registry/package.json +++ b/packages/shadcn-registry/package.json @@ -7,6 +7,7 @@ "devDependencies": { "@assistant-ui/react": "workspace:*", "@assistant-ui/react-markdown": "workspace:*", + "@assistant-ui/react-syntax-highlighter": "workspace:^", "@assistant-ui/tsconfig": "workspace:*", "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-icons": "^1.3.0", @@ -25,6 +26,7 @@ "react-syntax-highlighter": "^15.5.0", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", + "rehype-katex": "^7.0.0", "rimraf": "^5.0.7", "tailwind-merge": "^2.3.0", "tailwindcss": "^3.4.4", @@ -35,7 +37,5 @@ "scripts": { "build:registry": "tsx ./scripts/build-registry.ts" }, - "dependencies": { - "rehype-katex": "^7.0.0" - } + "dependencies": {} } diff --git a/packages/shadcn-registry/registry/assistant-ui/assistant-modal.tsx b/packages/shadcn-registry/registry/assistant-ui/assistant-modal.tsx index f8ddb2cbf..2851ea4f9 100644 --- a/packages/shadcn-registry/registry/assistant-ui/assistant-modal.tsx +++ b/packages/shadcn-registry/registry/assistant-ui/assistant-modal.tsx @@ -2,17 +2,12 @@ import { BotIcon, ChevronDownIcon } from "lucide-react"; -import { Thread } from "@/components/ui/assistant-ui/thread"; -import { Button } from "@/components/ui/button"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { TooltipProvider } from "@radix-ui/react-tooltip"; import { type FC, forwardRef } from "react"; import { AssistantModalPrimitive } from "@assistant-ui/react"; +import { Thread } from "@/components/ui/assistant-ui/thread"; +import { TooltipIconButton } from "@/components/ui/assistant-ui/tooltip-icon-button"; + export const AssistantModal: FC = () => { return ( @@ -40,31 +35,25 @@ const FloatingAssistantButton = forwardRef< const tooltip = state === "open" ? "Close Assistant" : "Open Assistant"; return ( - - - - - - {tooltip} - - + + + + + {tooltip} + ); }); diff --git a/packages/shadcn-registry/registry/assistant-ui/assistant-sidebar.tsx b/packages/shadcn-registry/registry/assistant-ui/assistant-sidebar.tsx index d85a807c2..3422163bf 100644 --- a/packages/shadcn-registry/registry/assistant-ui/assistant-sidebar.tsx +++ b/packages/shadcn-registry/registry/assistant-ui/assistant-sidebar.tsx @@ -6,7 +6,8 @@ import { ResizablePanelGroup, } from "@/components/ui/resizable"; import type { FC, PropsWithChildren } from "react"; -import { Thread } from "./thread"; + +import { Thread } from "@/components/ui/assistant-ui/thread"; export const AssistantSidebar: FC = ({ children }) => { return ( diff --git a/packages/shadcn-registry/registry/assistant-ui/experimental/codeblock.tsx b/packages/shadcn-registry/registry/assistant-ui/experimental/codeblock.tsx deleted file mode 100644 index a33650ce2..000000000 --- a/packages/shadcn-registry/registry/assistant-ui/experimental/codeblock.tsx +++ /dev/null @@ -1,152 +0,0 @@ -// 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 = 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 ( - - {value} - - ); -}); -CodeBlock.displayName = "CodeBlock"; - -export { CodeBlock }; - -export interface useCopyToClipboardProps { - timeout?: number; -} - -export function useCopyToClipboard({ - timeout = 2000, -}: useCopyToClipboardProps) { - const [isCopied, setIsCopied] = useState(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 }; -} diff --git a/packages/shadcn-registry/registry/assistant-ui/full/thread.tsx b/packages/shadcn-registry/registry/assistant-ui/full/thread.tsx index cc2e608a9..998663bf3 100644 --- a/packages/shadcn-registry/registry/assistant-ui/full/thread.tsx +++ b/packages/shadcn-registry/registry/assistant-ui/full/thread.tsx @@ -11,14 +11,7 @@ import { import type { FC } from "react"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import { Button, type ButtonProps } from "@/components/ui/button"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, - TooltipProvider, -} from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; import { ArrowDownIcon, CheckIcon, @@ -30,29 +23,29 @@ import { SendHorizonalIcon, } from "lucide-react"; import { MarkdownText } from "@/components/ui/assistant-ui/markdown-text"; +import { TooltipIconButton } from "@/components/ui/assistant-ui/tooltip-icon-button"; +import { cn } from "@/lib/utils"; export const Thread: FC = () => { return ( - - - - - - - -
- - -
-
-
-
+ + + + + + +
+ + +
+
+
); }; @@ -228,32 +221,6 @@ const BranchPicker: FC = ({ ); }; -type TooltipIconButtonProps = ButtonProps & { tooltip: string }; - -const TooltipIconButton: FC = ({ - children, - tooltip, - className, - ...rest -}) => { - return ( - - - - - {tooltip} - - ); -}; - const CircleStopIcon = () => { return ( { {...props} /> ), - code(props) { - const { children, className, node, ref, ...rest } = props; - const match = /language-(\w+)/.exec(className || "")?.[1]; + code: ({ node, className, ...props }) => { return ( - <> -
-

{match}

-
- - {children} - - + ); }, + CodeHeader, + SyntaxHighlighter, }} /> ); }; export const MarkdownText = memo(MarkdownTextImpl); + +const CodeHeader: FC = ({ language, code }) => { + const { isCopied, copyToClipboard } = useCopyToClipboard(); + const onCopy = () => { + if (!code || isCopied) return; + copyToClipboard(code); + }; + + return ( +
+ {language} + + {!isCopied && } + {isCopied && } + +
+ ); +}; + +const useCopyToClipboard = ({ + copiedDuration = 3000, +}: { + copiedDuration?: number; +} = {}) => { + const [isCopied, setIsCopied] = useState(false); + + const copyToClipboard = (value: string) => { + if (!value) return; + + navigator.clipboard.writeText(value).then(() => { + setIsCopied(true); + setTimeout(() => setIsCopied(false), copiedDuration); + }); + }; + + return { isCopied, copyToClipboard }; +}; diff --git a/packages/shadcn-registry/registry/assistant-ui/syntax-highlighter.tsx b/packages/shadcn-registry/registry/assistant-ui/syntax-highlighter.tsx new file mode 100644 index 000000000..95611741b --- /dev/null +++ b/packages/shadcn-registry/registry/assistant-ui/syntax-highlighter.tsx @@ -0,0 +1,24 @@ +import { PrismAsyncLight } from "react-syntax-highlighter"; +import { makePrismAsyncLightSyntaxHighlighter } from "@assistant-ui/react-syntax-highlighter"; + +import tsx from "react-syntax-highlighter/dist/esm/languages/prism/tsx"; +import python from "react-syntax-highlighter/dist/esm/languages/prism/python"; + +import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; + +// register languages you want to support +PrismAsyncLight.registerLanguage("js", tsx); +PrismAsyncLight.registerLanguage("jsx", tsx); +PrismAsyncLight.registerLanguage("ts", tsx); +PrismAsyncLight.registerLanguage("tsx", tsx); +PrismAsyncLight.registerLanguage("python", python); + +export const SyntaxHighlighter = makePrismAsyncLightSyntaxHighlighter({ + style: coldarkDark, + customStyle: { + margin: 0, + width: "100%", + background: "transparent", + padding: "1.5rem 1rem", + }, +}); diff --git a/packages/shadcn-registry/registry/assistant-ui/thread.tsx b/packages/shadcn-registry/registry/assistant-ui/thread.tsx index 96d0df8b9..8d923cae0 100644 --- a/packages/shadcn-registry/registry/assistant-ui/thread.tsx +++ b/packages/shadcn-registry/registry/assistant-ui/thread.tsx @@ -6,38 +6,29 @@ import { ThreadPrimitive, } from "@assistant-ui/react"; import type { FC } from "react"; +import { SendHorizonalIcon } from "lucide-react"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import { Button } from "@/components/ui/button"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, - TooltipProvider, -} from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; -import { SendHorizonalIcon } from "lucide-react"; +import { TooltipIconButton } from "@/components/ui/assistant-ui/tooltip-icon-button"; export const Thread: FC = () => { return ( - - - - + + + - + -
- -
-
-
-
+
+ +
+ + ); }; @@ -63,22 +54,15 @@ const Composer: FC = () => { rows={1} className="placeholder:text-muted-foreground size-full max-h-40 resize-none bg-transparent p-4 pr-12 text-sm outline-none" /> - - - - - - - Send - + + + + + ); }; diff --git a/packages/shadcn-registry/registry/assistant-ui/tooltip-icon-button.tsx b/packages/shadcn-registry/registry/assistant-ui/tooltip-icon-button.tsx new file mode 100644 index 000000000..23bc2b046 --- /dev/null +++ b/packages/shadcn-registry/registry/assistant-ui/tooltip-icon-button.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { forwardRef } from "react"; + +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Button, ButtonProps } from "@/components/ui/button"; + +export type TooltipIconButtonProps = ButtonProps & { + tooltip: string; + side?: "top" | "bottom" | "left" | "right"; +}; + +export const TooltipIconButton = forwardRef< + HTMLButtonElement, + TooltipIconButtonProps +>(({ children, tooltip, side = "bottom", ...rest }, ref) => { + return ( + + + + + + {tooltip} + + + ); +}); + +TooltipIconButton.displayName = "TooltipIconButton"; diff --git a/packages/shadcn-registry/registry/registry.ts b/packages/shadcn-registry/registry/registry.ts index 422272571..0724d5d06 100644 --- a/packages/shadcn-registry/registry/registry.ts +++ b/packages/shadcn-registry/registry/registry.ts @@ -12,35 +12,35 @@ export const registry: RegistryIndex = [ name: "modal", type: "components:ui", files: ["assistant-ui/assistant-modal.tsx"], - registryDependencies: ["thread", "button", "tooltip"], + registryDependencies: ["thread", "tooltip-icon-button"], dependencies: ["@assistant-ui/react", "lucide-react"], }, { name: "thread", type: "components:ui", files: ["assistant-ui/thread.tsx"], - registryDependencies: ["avatar", "button", "tooltip"], + registryDependencies: ["avatar", "tooltip-icon-button"], dependencies: ["@assistant-ui/react", "lucide-react"], }, { - name: "unstable-codeblock", + name: "syntax-highlighter", type: "components:ui", - files: ["assistant-ui/experimental/codeblock.tsx"], - registryDependencies: ["button"], + files: ["assistant-ui/syntax-highlighter.tsx"], dependencies: [ + "@assistant-ui/react-syntax-highlighter", "react-syntax-highlighter", "@types/react-syntax-highlighter", - "lucide-react", ], }, { name: "markdown-text", type: "components:ui", files: ["assistant-ui/markdown-text.tsx"], - // registryDependencies: ["unstable-codeblock"], + registryDependencies: ["tooltip-icon-button", "syntax-highlighter"], dependencies: [ "@assistant-ui/react", "@assistant-ui/react-markdown", + "lucide-react", "remark-gfm", "remark-math", "rehype-katex", @@ -50,7 +50,19 @@ export const registry: RegistryIndex = [ name: "thread-full", type: "components:ui", files: ["assistant-ui/full/thread.tsx"], - registryDependencies: ["markdown-text", "button", "avatar", "tooltip"], + registryDependencies: [ + "markdown-text", + "button", + "avatar", + "tooltip-icon-button", + ], + dependencies: ["@assistant-ui/react", "lucide-react"], + }, + { + name: "tooltip-icon-button", + type: "components:ui", + files: ["assistant-ui/tooltip-icon-button.tsx"], + registryDependencies: ["button", "tooltip"], dependencies: ["@assistant-ui/react", "lucide-react"], }, ];