From bd215d1eb519b65f2bc32eba478d36820a604c72 Mon Sep 17 00:00:00 2001 From: Igor Klepacki Date: Fri, 1 Nov 2024 12:30:39 +0100 Subject: [PATCH 1/3] fix(md): adjust overflow-x and add copy button to code --- apps/www/registry/default/ui/chat-message.tsx | 10 +-- .../registry/default/ui/markdown-renderer.tsx | 67 +++++++++++++++++-- 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/apps/www/registry/default/ui/chat-message.tsx b/apps/www/registry/default/ui/chat-message.tsx index 460c040..c20eb64 100644 --- a/apps/www/registry/default/ui/chat-message.tsx +++ b/apps/www/registry/default/ui/chat-message.tsx @@ -16,9 +16,9 @@ const chatBubbleVariants = cva( }, animation: { none: "", - slide: "duration-300 animate-in fade-in-0", - scale: "duration-300 animate-in fade-in-0 zoom-in-75", - fade: "duration-500 animate-in fade-in-0", + slide: "animate-in fade-in-0 duration-300", + scale: "animate-in fade-in-0 zoom-in-75 duration-300", + fade: "animate-in fade-in-0 duration-500", }, }, compoundVariants: [ @@ -85,7 +85,7 @@ export const ChatMessage: React.FC = ({ {role === "assistant" && actions ? ( -
+
{actions}
) : null} @@ -95,7 +95,7 @@ export const ChatMessage: React.FC = ({ {formattedTime} diff --git a/apps/www/registry/default/ui/markdown-renderer.tsx b/apps/www/registry/default/ui/markdown-renderer.tsx index 31e8b91..f29dddd 100644 --- a/apps/www/registry/default/ui/markdown-renderer.tsx +++ b/apps/www/registry/default/ui/markdown-renderer.tsx @@ -1,6 +1,10 @@ +import React from "react" import Markdown from "react-markdown" import remarkGfm from "remark-gfm" +import { cn } from "@/lib/utils" +import { CopyButton } from "@/registry/default/ui/copy-button" + interface MarkdownRendererProps { children: string } @@ -17,6 +21,51 @@ export function MarkdownRenderer({ children }: MarkdownRendererProps) { ) } +const CodeBlock = ({ children, className, ...restProps }: any) => { + const code = + typeof children === "string" + ? children + : childrenTakeAllStringContents(children) + + return ( +
+
+        {children}
+      
+ +
+ +
+
+ ) +} + +function childrenTakeAllStringContents(element: any): string { + if (typeof element === "string") { + return element + } + + if (element?.props?.children) { + let children = element.props.children + + if (Array.isArray(children)) { + return children + .map((child) => childrenTakeAllStringContents(child)) + .join("") + } else { + return childrenTakeAllStringContents(children) + } + } + + return "" +} + const COMPONENTS = { h1: withClass("h1", "text-2xl font-semibold"), h2: withClass("h2", "font-semibold text-xl"), @@ -27,15 +76,23 @@ const COMPONENTS = { a: withClass("a", "text-primary underline underline-offset-2"), blockquote: withClass("blockquote", "border-l-2 border-primary pl-4"), code: ({ children, className, node, ...rest }: any) => { - return ( + const match = /language-(\w+)/.exec(className || "") + return match ? ( + + {children} + + ) : ( &]:bg-background/50 font-mono [:not(pre)>&]:rounded-md [:not(pre)>&]:px-1 [:not(pre)>&]:py-0.5" + )} {...rest} > {children} ) }, + pre: ({ children }: any) => children, ol: withClass("ol", "list-decimal space-y-2 pl-6"), ul: withClass("ul", "list-disc space-y-2 pl-6"), li: withClass("li", "my-1.5"), @@ -54,10 +111,6 @@ const COMPONENTS = { tr: withClass("tr", "m-0 border-t p-0 even:bg-muted"), p: withClass("p", "whitespace-pre-wrap"), hr: withClass("hr", "border-foreground/20"), - pre: withClass( - "pre", - "rounded-md bg-background/50 p-4 font-mono text-sm border" - ), } function withClass(Tag: keyof JSX.IntrinsicElements, classes: string) { @@ -67,3 +120,5 @@ function withClass(Tag: keyof JSX.IntrinsicElements, classes: string) { Component.displayName = Tag return Component } + +export default MarkdownRenderer From a02ca4df1aab03a7a8ad5731ba9079ac670cc1d3 Mon Sep 17 00:00:00 2001 From: Igor Klepacki Date: Fri, 1 Nov 2024 12:31:34 +0100 Subject: [PATCH 2/3] chore(registry): build --- apps/www/public/r/chat-message.json | 2 +- apps/www/public/r/markdown-renderer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/www/public/r/chat-message.json b/apps/www/public/r/chat-message.json index bcadba9..0e4aa5c 100644 --- a/apps/www/public/r/chat-message.json +++ b/apps/www/public/r/chat-message.json @@ -9,7 +9,7 @@ "files": [ { "path": "ui/chat-message.tsx", - "content": "\"use client\"\n\nimport React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { MarkdownRenderer } from \"@/registry/default/ui/markdown-renderer\"\n\nconst chatBubbleVariants = cva(\n \"group/message relative break-words rounded-lg p-3 text-sm sm:max-w-[70%]\",\n {\n variants: {\n isUser: {\n true: \"bg-primary\",\n false: \"bg-muted\",\n },\n animation: {\n none: \"\",\n slide: \"duration-300 animate-in fade-in-0\",\n scale: \"duration-300 animate-in fade-in-0 zoom-in-75\",\n fade: \"duration-500 animate-in fade-in-0\",\n },\n },\n compoundVariants: [\n {\n isUser: true,\n animation: \"slide\",\n class: \"slide-in-from-right\",\n },\n {\n isUser: false,\n animation: \"slide\",\n class: \"slide-in-from-left\",\n },\n {\n isUser: true,\n animation: \"scale\",\n class: \"origin-bottom-right\",\n },\n {\n isUser: false,\n animation: \"scale\",\n class: \"origin-bottom-left\",\n },\n ],\n }\n)\n\ntype Animation = VariantProps[\"animation\"]\n\nexport interface Message {\n id: string\n role: \"user\" | \"assistant\" | (string & {})\n content: string\n createdAt?: Date\n attachments?: File[]\n}\n\nexport interface ChatMessageProps extends Message {\n showTimeStamp?: boolean\n animation?: Animation\n actions?: React.ReactNode\n}\n\nexport const ChatMessage: React.FC = ({\n role,\n content,\n createdAt,\n showTimeStamp = false,\n animation = \"scale\",\n actions,\n}) => {\n const isUser = role === \"user\"\n\n const formattedTime = createdAt?.toLocaleTimeString(\"en-US\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n })\n\n return (\n
\n
\n
\n {content}\n
\n\n {role === \"assistant\" && actions ? (\n
\n {actions}\n
\n ) : null}\n
\n\n {showTimeStamp && createdAt ? (\n \n {formattedTime}\n \n ) : null}\n
\n )\n}\n", + "content": "\"use client\"\n\nimport React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { MarkdownRenderer } from \"@/registry/default/ui/markdown-renderer\"\n\nconst chatBubbleVariants = cva(\n \"group/message relative break-words rounded-lg p-3 text-sm sm:max-w-[70%]\",\n {\n variants: {\n isUser: {\n true: \"bg-primary\",\n false: \"bg-muted\",\n },\n animation: {\n none: \"\",\n slide: \"animate-in fade-in-0 duration-300\",\n scale: \"animate-in fade-in-0 zoom-in-75 duration-300\",\n fade: \"animate-in fade-in-0 duration-500\",\n },\n },\n compoundVariants: [\n {\n isUser: true,\n animation: \"slide\",\n class: \"slide-in-from-right\",\n },\n {\n isUser: false,\n animation: \"slide\",\n class: \"slide-in-from-left\",\n },\n {\n isUser: true,\n animation: \"scale\",\n class: \"origin-bottom-right\",\n },\n {\n isUser: false,\n animation: \"scale\",\n class: \"origin-bottom-left\",\n },\n ],\n }\n)\n\ntype Animation = VariantProps[\"animation\"]\n\nexport interface Message {\n id: string\n role: \"user\" | \"assistant\" | (string & {})\n content: string\n createdAt?: Date\n attachments?: File[]\n}\n\nexport interface ChatMessageProps extends Message {\n showTimeStamp?: boolean\n animation?: Animation\n actions?: React.ReactNode\n}\n\nexport const ChatMessage: React.FC = ({\n role,\n content,\n createdAt,\n showTimeStamp = false,\n animation = \"scale\",\n actions,\n}) => {\n const isUser = role === \"user\"\n\n const formattedTime = createdAt?.toLocaleTimeString(\"en-US\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n })\n\n return (\n
\n
\n
\n {content}\n
\n\n {role === \"assistant\" && actions ? (\n
\n {actions}\n
\n ) : null}\n
\n\n {showTimeStamp && createdAt ? (\n \n {formattedTime}\n \n ) : null}\n
\n )\n}\n", "type": "registry:ui", "target": "" } diff --git a/apps/www/public/r/markdown-renderer.json b/apps/www/public/r/markdown-renderer.json index 53e4e5a..772bddc 100644 --- a/apps/www/public/r/markdown-renderer.json +++ b/apps/www/public/r/markdown-renderer.json @@ -8,7 +8,7 @@ "files": [ { "path": "ui/markdown-renderer.tsx", - "content": "import Markdown from \"react-markdown\"\nimport remarkGfm from \"remark-gfm\"\n\ninterface MarkdownRendererProps {\n children: string\n}\n\nexport function MarkdownRenderer({ children }: MarkdownRendererProps) {\n return (\n \n {children}\n \n )\n}\n\nconst COMPONENTS = {\n h1: withClass(\"h1\", \"text-2xl font-semibold\"),\n h2: withClass(\"h2\", \"font-semibold text-xl\"),\n h3: withClass(\"h3\", \"font-semibold text-lg\"),\n h4: withClass(\"h4\", \"font-semibold text-base\"),\n h5: withClass(\"h5\", \"font-medium\"),\n strong: withClass(\"strong\", \"font-semibold\"),\n a: withClass(\"a\", \"text-primary underline underline-offset-2\"),\n blockquote: withClass(\"blockquote\", \"border-l-2 border-primary pl-4\"),\n code: ({ children, className, node, ...rest }: any) => {\n return (\n &]:rounded-md [:not(pre)>&]:bg-background/50 [:not(pre)>&]:px-1 [:not(pre)>&]:py-0.5\"\n {...rest}\n >\n {children}\n \n )\n },\n ol: withClass(\"ol\", \"list-decimal space-y-2 pl-6\"),\n ul: withClass(\"ul\", \"list-disc space-y-2 pl-6\"),\n li: withClass(\"li\", \"my-1.5\"),\n table: withClass(\n \"table\",\n \"w-full border-collapse overflow-y-auto rounded-md border border-foreground/20\"\n ),\n th: withClass(\n \"th\",\n \"border border-foreground/20 px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right\"\n ),\n td: withClass(\n \"td\",\n \"border border-foreground/20 px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right\"\n ),\n tr: withClass(\"tr\", \"m-0 border-t p-0 even:bg-muted\"),\n p: withClass(\"p\", \"whitespace-pre-wrap\"),\n hr: withClass(\"hr\", \"border-foreground/20\"),\n pre: withClass(\n \"pre\",\n \"rounded-md bg-background/50 p-4 font-mono text-sm border\"\n ),\n}\n\nfunction withClass(Tag: keyof JSX.IntrinsicElements, classes: string) {\n const Component = ({ node, ...props }: any) => (\n \n )\n Component.displayName = Tag\n return Component\n}\n", + "content": "import React from \"react\"\nimport Markdown from \"react-markdown\"\nimport remarkGfm from \"remark-gfm\"\n\nimport { cn } from \"@/lib/utils\"\nimport { CopyButton } from \"@/registry/default/ui/copy-button\"\n\ninterface MarkdownRendererProps {\n children: string\n}\n\nexport function MarkdownRenderer({ children }: MarkdownRendererProps) {\n return (\n \n {children}\n \n )\n}\n\nconst CodeBlock = ({ children, className, ...restProps }: any) => {\n const code =\n typeof children === \"string\"\n ? children\n : childrenTakeAllStringContents(children)\n\n return (\n
\n \n {children}\n \n\n
\n \n
\n
\n )\n}\n\nfunction childrenTakeAllStringContents(element: any): string {\n if (typeof element === \"string\") {\n return element\n }\n\n if (element?.props?.children) {\n let children = element.props.children\n\n if (Array.isArray(children)) {\n return children\n .map((child) => childrenTakeAllStringContents(child))\n .join(\"\")\n } else {\n return childrenTakeAllStringContents(children)\n }\n }\n\n return \"\"\n}\n\nconst COMPONENTS = {\n h1: withClass(\"h1\", \"text-2xl font-semibold\"),\n h2: withClass(\"h2\", \"font-semibold text-xl\"),\n h3: withClass(\"h3\", \"font-semibold text-lg\"),\n h4: withClass(\"h4\", \"font-semibold text-base\"),\n h5: withClass(\"h5\", \"font-medium\"),\n strong: withClass(\"strong\", \"font-semibold\"),\n a: withClass(\"a\", \"text-primary underline underline-offset-2\"),\n blockquote: withClass(\"blockquote\", \"border-l-2 border-primary pl-4\"),\n code: ({ children, className, node, ...rest }: any) => {\n const match = /language-(\\w+)/.exec(className || \"\")\n return match ? (\n \n {children}\n \n ) : (\n &]:bg-background/50 font-mono [:not(pre)>&]:rounded-md [:not(pre)>&]:px-1 [:not(pre)>&]:py-0.5\"\n )}\n {...rest}\n >\n {children}\n \n )\n },\n pre: ({ children }: any) => children,\n ol: withClass(\"ol\", \"list-decimal space-y-2 pl-6\"),\n ul: withClass(\"ul\", \"list-disc space-y-2 pl-6\"),\n li: withClass(\"li\", \"my-1.5\"),\n table: withClass(\n \"table\",\n \"w-full border-collapse overflow-y-auto rounded-md border border-foreground/20\"\n ),\n th: withClass(\n \"th\",\n \"border border-foreground/20 px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right\"\n ),\n td: withClass(\n \"td\",\n \"border border-foreground/20 px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right\"\n ),\n tr: withClass(\"tr\", \"m-0 border-t p-0 even:bg-muted\"),\n p: withClass(\"p\", \"whitespace-pre-wrap\"),\n hr: withClass(\"hr\", \"border-foreground/20\"),\n}\n\nfunction withClass(Tag: keyof JSX.IntrinsicElements, classes: string) {\n const Component = ({ node, ...props }: any) => (\n \n )\n Component.displayName = Tag\n return Component\n}\n\nexport default MarkdownRenderer\n", "type": "registry:ui", "target": "" } From 6bb58645b297ba3288e15dc597d4f8f5feab8959 Mon Sep 17 00:00:00 2001 From: Igor Klepacki Date: Fri, 1 Nov 2024 12:41:36 +0100 Subject: [PATCH 3/3] feat(md): move copy button inside the code container --- apps/www/public/r/markdown-renderer.json | 2 +- apps/www/registry/default/ui/markdown-renderer.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/www/public/r/markdown-renderer.json b/apps/www/public/r/markdown-renderer.json index 772bddc..c8439ee 100644 --- a/apps/www/public/r/markdown-renderer.json +++ b/apps/www/public/r/markdown-renderer.json @@ -8,7 +8,7 @@ "files": [ { "path": "ui/markdown-renderer.tsx", - "content": "import React from \"react\"\nimport Markdown from \"react-markdown\"\nimport remarkGfm from \"remark-gfm\"\n\nimport { cn } from \"@/lib/utils\"\nimport { CopyButton } from \"@/registry/default/ui/copy-button\"\n\ninterface MarkdownRendererProps {\n children: string\n}\n\nexport function MarkdownRenderer({ children }: MarkdownRendererProps) {\n return (\n \n {children}\n \n )\n}\n\nconst CodeBlock = ({ children, className, ...restProps }: any) => {\n const code =\n typeof children === \"string\"\n ? children\n : childrenTakeAllStringContents(children)\n\n return (\n
\n \n {children}\n \n\n
\n \n
\n
\n )\n}\n\nfunction childrenTakeAllStringContents(element: any): string {\n if (typeof element === \"string\") {\n return element\n }\n\n if (element?.props?.children) {\n let children = element.props.children\n\n if (Array.isArray(children)) {\n return children\n .map((child) => childrenTakeAllStringContents(child))\n .join(\"\")\n } else {\n return childrenTakeAllStringContents(children)\n }\n }\n\n return \"\"\n}\n\nconst COMPONENTS = {\n h1: withClass(\"h1\", \"text-2xl font-semibold\"),\n h2: withClass(\"h2\", \"font-semibold text-xl\"),\n h3: withClass(\"h3\", \"font-semibold text-lg\"),\n h4: withClass(\"h4\", \"font-semibold text-base\"),\n h5: withClass(\"h5\", \"font-medium\"),\n strong: withClass(\"strong\", \"font-semibold\"),\n a: withClass(\"a\", \"text-primary underline underline-offset-2\"),\n blockquote: withClass(\"blockquote\", \"border-l-2 border-primary pl-4\"),\n code: ({ children, className, node, ...rest }: any) => {\n const match = /language-(\\w+)/.exec(className || \"\")\n return match ? (\n \n {children}\n \n ) : (\n &]:bg-background/50 font-mono [:not(pre)>&]:rounded-md [:not(pre)>&]:px-1 [:not(pre)>&]:py-0.5\"\n )}\n {...rest}\n >\n {children}\n \n )\n },\n pre: ({ children }: any) => children,\n ol: withClass(\"ol\", \"list-decimal space-y-2 pl-6\"),\n ul: withClass(\"ul\", \"list-disc space-y-2 pl-6\"),\n li: withClass(\"li\", \"my-1.5\"),\n table: withClass(\n \"table\",\n \"w-full border-collapse overflow-y-auto rounded-md border border-foreground/20\"\n ),\n th: withClass(\n \"th\",\n \"border border-foreground/20 px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right\"\n ),\n td: withClass(\n \"td\",\n \"border border-foreground/20 px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right\"\n ),\n tr: withClass(\"tr\", \"m-0 border-t p-0 even:bg-muted\"),\n p: withClass(\"p\", \"whitespace-pre-wrap\"),\n hr: withClass(\"hr\", \"border-foreground/20\"),\n}\n\nfunction withClass(Tag: keyof JSX.IntrinsicElements, classes: string) {\n const Component = ({ node, ...props }: any) => (\n \n )\n Component.displayName = Tag\n return Component\n}\n\nexport default MarkdownRenderer\n", + "content": "import React from \"react\"\nimport Markdown from \"react-markdown\"\nimport remarkGfm from \"remark-gfm\"\n\nimport { cn } from \"@/lib/utils\"\nimport { CopyButton } from \"@/registry/default/ui/copy-button\"\n\ninterface MarkdownRendererProps {\n children: string\n}\n\nexport function MarkdownRenderer({ children }: MarkdownRendererProps) {\n return (\n \n {children}\n \n )\n}\n\nconst CodeBlock = ({ children, className, ...restProps }: any) => {\n const code =\n typeof children === \"string\"\n ? children\n : childrenTakeAllStringContents(children)\n\n return (\n
\n \n {children}\n \n\n
\n \n
\n
\n )\n}\n\nfunction childrenTakeAllStringContents(element: any): string {\n if (typeof element === \"string\") {\n return element\n }\n\n if (element?.props?.children) {\n let children = element.props.children\n\n if (Array.isArray(children)) {\n return children\n .map((child) => childrenTakeAllStringContents(child))\n .join(\"\")\n } else {\n return childrenTakeAllStringContents(children)\n }\n }\n\n return \"\"\n}\n\nconst COMPONENTS = {\n h1: withClass(\"h1\", \"text-2xl font-semibold\"),\n h2: withClass(\"h2\", \"font-semibold text-xl\"),\n h3: withClass(\"h3\", \"font-semibold text-lg\"),\n h4: withClass(\"h4\", \"font-semibold text-base\"),\n h5: withClass(\"h5\", \"font-medium\"),\n strong: withClass(\"strong\", \"font-semibold\"),\n a: withClass(\"a\", \"text-primary underline underline-offset-2\"),\n blockquote: withClass(\"blockquote\", \"border-l-2 border-primary pl-4\"),\n code: ({ children, className, node, ...rest }: any) => {\n const match = /language-(\\w+)/.exec(className || \"\")\n return match ? (\n \n {children}\n \n ) : (\n &]:bg-background/50 font-mono [:not(pre)>&]:rounded-md [:not(pre)>&]:px-1 [:not(pre)>&]:py-0.5\"\n )}\n {...rest}\n >\n {children}\n \n )\n },\n pre: ({ children }: any) => children,\n ol: withClass(\"ol\", \"list-decimal space-y-2 pl-6\"),\n ul: withClass(\"ul\", \"list-disc space-y-2 pl-6\"),\n li: withClass(\"li\", \"my-1.5\"),\n table: withClass(\n \"table\",\n \"w-full border-collapse overflow-y-auto rounded-md border border-foreground/20\"\n ),\n th: withClass(\n \"th\",\n \"border border-foreground/20 px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right\"\n ),\n td: withClass(\n \"td\",\n \"border border-foreground/20 px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right\"\n ),\n tr: withClass(\"tr\", \"m-0 border-t p-0 even:bg-muted\"),\n p: withClass(\"p\", \"whitespace-pre-wrap\"),\n hr: withClass(\"hr\", \"border-foreground/20\"),\n}\n\nfunction withClass(Tag: keyof JSX.IntrinsicElements, classes: string) {\n const Component = ({ node, ...props }: any) => (\n \n )\n Component.displayName = Tag\n return Component\n}\n\nexport default MarkdownRenderer\n", "type": "registry:ui", "target": "" } diff --git a/apps/www/registry/default/ui/markdown-renderer.tsx b/apps/www/registry/default/ui/markdown-renderer.tsx index f29dddd..3049a67 100644 --- a/apps/www/registry/default/ui/markdown-renderer.tsx +++ b/apps/www/registry/default/ui/markdown-renderer.tsx @@ -39,7 +39,7 @@ const CodeBlock = ({ children, className, ...restProps }: any) => { {children} -
+