diff --git a/apps/www/content/docs/components/chat.mdx b/apps/www/content/docs/components/chat.mdx
index edb9ee0..dfe4bcc 100644
--- a/apps/www/content/docs/components/chat.mdx
+++ b/apps/www/content/docs/components/chat.mdx
@@ -34,7 +34,7 @@ The Chat component provides a complete chat interface with message history, typi
```bash
-npx shadcn@latest add chat
+npx shadcn@latest add https://shadcn-chatbot-kit.vercel.app/r/chat.json
```
diff --git a/apps/www/content/docs/components/message-input.mdx b/apps/www/content/docs/components/message-input.mdx
index f1f750f..bc775e7 100644
--- a/apps/www/content/docs/components/message-input.mdx
+++ b/apps/www/content/docs/components/message-input.mdx
@@ -33,7 +33,7 @@ The MessageInput component provides a rich textarea experience with support for
```bash
-npx shadcn@latest add message-input
+npx shadcn@latest add https://shadcn-chatbot-kit.vercel.app/r/message-input.json
```
@@ -211,14 +211,6 @@ Show stop button during generation:
```
-## Design
-
-1. The component uses Framer Motion for smooth animations
-2. File attachments are displayed as removable chips
-3. The textarea auto-resizes for better user experience
-4. The submit button is disabled when input is empty
-5. The submit button changes to a cancel button while generating, if `stop` prop is provided, if not it is disabled during generation
-
## Theming
The Chat component is using theme colors and fully themable with CSS variables.
diff --git a/apps/www/public/r/markdown-renderer.json b/apps/www/public/r/markdown-renderer.json
index 64dbec2..b4b2a01 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 // TODO fix anys\n code: ({ children, className, node, ...rest }: any) => {\n console.log(\"code renderer\", children)\n const match = /language-(\\w+)/.exec(className || \"\")\n return match ? (\n // TODO: syntax highlighting\n <>\n
{match[1]}
\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 &]: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(\"pre\", \"rounded-md bg-background/50 p-4 font-mono text-sm\"),\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 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(\"pre\", \"rounded-md bg-background/50 p-4 font-mono text-sm\"),\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",
"type": "registry:ui",
"target": ""
}
diff --git a/apps/www/public/r/message-input.json b/apps/www/public/r/message-input.json
index a8f4a32..b19f9e6 100644
--- a/apps/www/public/r/message-input.json
+++ b/apps/www/public/r/message-input.json
@@ -12,7 +12,7 @@
"files": [
{
"path": "ui/message-input.tsx",
- "content": "\"use client\"\n\nimport React, { useRef, useState } from \"react\"\nimport { AnimatePresence, motion } from \"framer-motion\"\nimport { ArrowUp, FileIcon, Paperclip, Square, X } from \"lucide-react\"\nimport { omit } from \"remeda\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useAutosizeTextArea } from \"@/registry/default/hooks/use-autosize-textarea\"\nimport { Button } from \"@/registry/default/ui/button\"\n\ninterface MessageInputBaseProps\n extends React.TextareaHTMLAttributes {\n value: string\n submitOnEnter?: boolean\n stop?: () => void\n isGenerating: boolean\n}\n\ninterface MessageInputWithoutAttachmentProps extends MessageInputBaseProps {\n allowAttachments?: false\n}\n\ninterface MessageInputWithAttachmentsProps extends MessageInputBaseProps {\n allowAttachments: true\n files: File[] | null\n setFiles: React.Dispatch>\n}\n\ntype MessageInputProps =\n | MessageInputWithoutAttachmentProps\n | MessageInputWithAttachmentsProps\n\nexport function MessageInput({\n placeholder = \"Ask AI...\",\n className,\n onKeyDown: onKeyDownProp,\n submitOnEnter = true,\n stop,\n isGenerating,\n ...props\n}: MessageInputProps) {\n const [isDragging, setIsDragging] = useState(false)\n\n const addFiles = (files: File[] | null) => {\n if (props.allowAttachments) {\n props.setFiles((currentFiles) => {\n if (currentFiles === null) {\n return files\n }\n\n if (files === null) {\n return currentFiles\n }\n\n return [...currentFiles, ...files]\n })\n }\n }\n\n const onDragOver = (event: React.DragEvent) => {\n if (props.allowAttachments !== true) return\n event.preventDefault()\n setIsDragging(true)\n }\n\n const onDragLeave = (event: React.DragEvent) => {\n if (props.allowAttachments !== true) return\n event.preventDefault()\n setIsDragging(false)\n }\n\n const onDrop = (event: React.DragEvent) => {\n setIsDragging(false)\n if (props.allowAttachments !== true) return\n event.preventDefault()\n const dataTransfer = event.dataTransfer\n if (dataTransfer.files.length) {\n addFiles(Array.from(dataTransfer.files))\n }\n }\n\n const onPaste = (event: React.ClipboardEvent) => {\n const items = event.clipboardData?.items\n if (!items) return\n\n const files = Array.from(items)\n .map((item) => item.getAsFile())\n .filter((file) => file !== null)\n\n if (props.allowAttachments && files.length > 0) {\n addFiles(files)\n }\n }\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n if (submitOnEnter && event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault()\n event.currentTarget.form?.requestSubmit()\n }\n\n onKeyDownProp?.(event)\n }\n\n const textAreaRef = useRef(null)\n\n const showFileList =\n props.allowAttachments && props.files && props.files.length > 0\n\n useAutosizeTextArea({\n ref: textAreaRef,\n maxHeight: 240,\n borderWidth: 1,\n dependencies: [props.value, showFileList],\n })\n\n return (\n \n
\n\n {props.allowAttachments && (\n
\n
\n
\n {props.files?.map((file) => {\n return (\n {\n props.setFiles((files) => {\n if (!files) return null\n\n const filtered = Array.from(files).filter(\n (f) => f !== file\n )\n if (filtered.length === 0) return null\n return filtered\n })\n }}\n />\n )\n })}\n \n
\n
\n )}\n\n
\n {props.allowAttachments && (\n
\n )}\n {isGenerating && stop ? (\n
\n ) : (\n
\n )}\n
\n\n {props.allowAttachments &&
}\n
\n )\n}\nMessageInput.displayName = \"MessageInput\"\n\ninterface FileUploadOverlayProps {\n isDragging: boolean\n}\n\nfunction FileUploadOverlay({ isDragging }: FileUploadOverlayProps) {\n return (\n \n {isDragging && (\n \n \n Drop your files here to attach them.\n \n )}\n \n )\n}\n\ninterface FilePreviewProps {\n file: File\n onRemove: () => void\n}\n\nconst FilePreview = React.forwardRef(\n (props, ref) => {\n if (props.file.type.startsWith(\"image/\")) {\n return \n }\n\n return \n }\n)\nFilePreview.displayName = \"FilePreview\"\n\nconst ImageFilePreview = React.forwardRef(\n ({ file, onRemove }, ref) => {\n return (\n \n \n {/* eslint-disable-next-line @next/next/no-img-element */}\n
\n
\n {file.name}\n \n
\n\n \n \n )\n }\n)\nImageFilePreview.displayName = \"ImageFilePreview\"\n\nconst GenericFilePreview = React.forwardRef(\n ({ file, onRemove }, ref) => {\n return (\n \n \n
\n \n
\n
\n {file.name}\n \n
\n\n \n \n )\n }\n)\nGenericFilePreview.displayName = \"GenericFilePreview\"\n\nfunction showFileUploadDialog() {\n const input = document.createElement(\"input\")\n\n input.type = \"file\"\n input.multiple = true\n // TODO: accept value\n // file.type.startsWith(\"image/\") || file.type.startsWith(\"text/\")\n input.accept = \"*/*\"\n input.click()\n\n return new Promise((resolve) => {\n input.onchange = (e) => {\n const files = (e.currentTarget as HTMLInputElement).files\n\n if (files) {\n resolve(Array.from(files))\n return\n }\n\n resolve(null)\n }\n })\n}\n",
+ "content": "\"use client\"\n\nimport React, { useRef, useState } from \"react\"\nimport { AnimatePresence, motion } from \"framer-motion\"\nimport { ArrowUp, FileIcon, Paperclip, Square, X } from \"lucide-react\"\nimport { omit } from \"remeda\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useAutosizeTextArea } from \"@/registry/default/hooks/use-autosize-textarea\"\nimport { Button } from \"@/registry/default/ui/button\"\n\ninterface MessageInputBaseProps\n extends React.TextareaHTMLAttributes {\n value: string\n submitOnEnter?: boolean\n stop?: () => void\n isGenerating: boolean\n}\n\ninterface MessageInputWithoutAttachmentProps extends MessageInputBaseProps {\n allowAttachments?: false\n}\n\ninterface MessageInputWithAttachmentsProps extends MessageInputBaseProps {\n allowAttachments: true\n files: File[] | null\n setFiles: React.Dispatch>\n}\n\ntype MessageInputProps =\n | MessageInputWithoutAttachmentProps\n | MessageInputWithAttachmentsProps\n\nexport function MessageInput({\n placeholder = \"Ask AI...\",\n className,\n onKeyDown: onKeyDownProp,\n submitOnEnter = true,\n stop,\n isGenerating,\n ...props\n}: MessageInputProps) {\n const [isDragging, setIsDragging] = useState(false)\n\n const addFiles = (files: File[] | null) => {\n if (props.allowAttachments) {\n props.setFiles((currentFiles) => {\n if (currentFiles === null) {\n return files\n }\n\n if (files === null) {\n return currentFiles\n }\n\n return [...currentFiles, ...files]\n })\n }\n }\n\n const onDragOver = (event: React.DragEvent) => {\n if (props.allowAttachments !== true) return\n event.preventDefault()\n setIsDragging(true)\n }\n\n const onDragLeave = (event: React.DragEvent) => {\n if (props.allowAttachments !== true) return\n event.preventDefault()\n setIsDragging(false)\n }\n\n const onDrop = (event: React.DragEvent) => {\n setIsDragging(false)\n if (props.allowAttachments !== true) return\n event.preventDefault()\n const dataTransfer = event.dataTransfer\n if (dataTransfer.files.length) {\n addFiles(Array.from(dataTransfer.files))\n }\n }\n\n const onPaste = (event: React.ClipboardEvent) => {\n const items = event.clipboardData?.items\n if (!items) return\n\n const files = Array.from(items)\n .map((item) => item.getAsFile())\n .filter((file) => file !== null)\n\n if (props.allowAttachments && files.length > 0) {\n addFiles(files)\n }\n }\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n if (submitOnEnter && event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault()\n event.currentTarget.form?.requestSubmit()\n }\n\n onKeyDownProp?.(event)\n }\n\n const textAreaRef = useRef(null)\n\n const showFileList =\n props.allowAttachments && props.files && props.files.length > 0\n\n useAutosizeTextArea({\n ref: textAreaRef,\n maxHeight: 240,\n borderWidth: 1,\n dependencies: [props.value, showFileList],\n })\n\n return (\n \n
\n\n {props.allowAttachments && (\n
\n
\n
\n {props.files?.map((file) => {\n return (\n {\n props.setFiles((files) => {\n if (!files) return null\n\n const filtered = Array.from(files).filter(\n (f) => f !== file\n )\n if (filtered.length === 0) return null\n return filtered\n })\n }}\n />\n )\n })}\n \n
\n
\n )}\n\n
\n {props.allowAttachments && (\n
\n )}\n {isGenerating && stop ? (\n
\n ) : (\n
\n )}\n
\n\n {props.allowAttachments &&
}\n
\n )\n}\nMessageInput.displayName = \"MessageInput\"\n\ninterface FileUploadOverlayProps {\n isDragging: boolean\n}\n\nfunction FileUploadOverlay({ isDragging }: FileUploadOverlayProps) {\n return (\n \n {isDragging && (\n \n \n Drop your files here to attach them.\n \n )}\n \n )\n}\n\ninterface FilePreviewProps {\n file: File\n onRemove: () => void\n}\n\nconst FilePreview = React.forwardRef(\n (props, ref) => {\n if (props.file.type.startsWith(\"image/\")) {\n return \n }\n\n return \n }\n)\nFilePreview.displayName = \"FilePreview\"\n\nconst ImageFilePreview = React.forwardRef(\n ({ file, onRemove }, ref) => {\n return (\n \n \n {/* eslint-disable-next-line @next/next/no-img-element */}\n
\n
\n {file.name}\n \n
\n\n \n \n )\n }\n)\nImageFilePreview.displayName = \"ImageFilePreview\"\n\nconst GenericFilePreview = React.forwardRef(\n ({ file, onRemove }, ref) => {\n return (\n \n \n
\n \n
\n
\n {file.name}\n \n
\n\n \n \n )\n }\n)\nGenericFilePreview.displayName = \"GenericFilePreview\"\n\nfunction showFileUploadDialog() {\n const input = document.createElement(\"input\")\n\n input.type = \"file\"\n input.multiple = true\n input.accept = \"*/*\"\n input.click()\n\n return new Promise((resolve) => {\n input.onchange = (e) => {\n const files = (e.currentTarget as HTMLInputElement).files\n\n if (files) {\n resolve(Array.from(files))\n return\n }\n\n resolve(null)\n }\n })\n}\n",
"type": "registry:ui",
"target": ""
}