[data-slot=chat-message-avatar])]:col-start-2 row-start-2 flex min-w-0 flex-col gap-1 group-data-[align=end]/message:items-end\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function ChatMessageFooter({\n className,\n ...props\n}: ComponentProps<\"div\">): ReactElement {\n return (\n
\n );\n}\n",
"type": "registry:component",
"target": "@components/chat/chat-message-layout.tsx"
},
+ {
+ "path": "registry/default/chat/chat-message-markdown.tsx",
+ "content": "import type { ComponentProps, ReactElement } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nexport function ChatMessageMarkdown({\n className,\n ...props\n}: ComponentProps<\"div\">): ReactElement {\n return (\n
:first-child]:mt-0 [&>:last-child]:mb-0\",\n \"[&_:is(h1,h2,h3,h4,h5,h6)]:mt-4 [&_:is(h1,h2,h3,h4,h5,h6)]:mb-2 [&_:is(h1,h2,h3,h4,h5,h6)]:font-semibold [&_:is(h1,h2,h3,h4,h5,h6)]:leading-snug [&_:is(p,ul,ol,pre,blockquote,table)]:my-3 [&_h1]:text-lg [&_h2]:text-base [&_h6]:text-muted-foreground\",\n \"[&_a:hover]:decoration-foreground [&_a]:underline [&_a]:decoration-foreground/32 [&_a]:underline-offset-2 [&_hr]:my-4 [&_strong]:font-semibold\",\n \"[&_:is(ul,ol)]:ps-5 [&_li+li]:mt-1 [&_li>:is(ul,ol)]:mt-1 [&_li>:is(ul,ol)]:mb-0 [&_li>p]:my-0 [&_li]:marker:text-muted-foreground [&_ol>li]:marker:tabular-nums [&_ol]:list-decimal [&_ul]:list-disc\",\n \"[&_:not(pre)>code]:rounded-sm [&_:not(pre)>code]:bg-foreground/8 [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5 [&_code]:font-mono [&_code]:text-[.875em] [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:border [&_pre]:bg-foreground/4 [&_pre]:px-3 [&_pre]:py-2.5 [&_pre]:leading-normal\",\n \"[&_blockquote]:border-s-2 [&_blockquote]:ps-3 [&_blockquote]:text-muted-foreground\",\n \"has-[>table]:overflow-x-auto [&_:has(>table)]:overflow-x-auto [&_:is(th,td):first-child]:ps-0 [&_:is(th,td):last-child]:pe-0 [&_:is(th,td)]:px-3 [&_:is(th,td)]:py-1.5 [&_table]:border-collapse [&_tbody_tr:last-child]:border-b-0 [&_td]:align-top [&_td]:tabular-nums [&_th]:text-start [&_th]:align-bottom [&_th]:font-medium [&_tr]:border-b\",\n className,\n )}\n {...props}\n />\n );\n}\n",
+ "type": "registry:component",
+ "target": "@components/chat/chat-message-markdown.tsx"
+ },
{
"path": "registry/default/chat/chat-message-text.tsx",
- "content": "import type { ComponentProps, ReactElement } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nexport type ChatMessageTextProps = Omit
, \"children\"> & {\n text: string;\n mentions?: readonly string[];\n};\n\nexport function ChatMessageText({\n text,\n mentions = [],\n className,\n ...props\n}: ChatMessageTextProps): ReactElement {\n const names = new Set(mentions.map((name) => `@${name}`));\n const parts = text.split(/(@[\\p{L}\\p{N}_.-]+)/u);\n return (\n \n {parts.map((part, index) => {\n if (index > 0 && !/(?:^|\\s)$/.test(parts[index - 1] ?? \"\")) return part;\n const mention = names.has(part) ? part : part.replace(/[.-]+$/, \"\");\n if (!names.has(mention)) return part;\n return (\n \n {mention} \n {part.slice(mention.length)}\n \n );\n })}\n
\n );\n}\n",
+ "content": "import type { ComponentProps, ReactElement } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nexport type ChatMessageTextProps = ComponentProps<\"p\"> & {\n text: string;\n mentions?: readonly string[];\n};\n\nexport function ChatMessageText({\n text,\n mentions = [],\n children,\n className,\n ...props\n}: ChatMessageTextProps): ReactElement {\n const names = new Set(mentions.map((name) => `@${name}`));\n const parts = text.split(/(@[\\p{L}\\p{N}_.-]+)/u);\n return (\n \n {parts.map((part, index) => {\n if (index > 0 && !/(?:^|\\s)$/.test(parts[index - 1] ?? \"\")) return part;\n const mention = names.has(part) ? part : part.replace(/[.-]+$/, \"\");\n if (!names.has(mention)) return part;\n return (\n \n {mention} \n {part.slice(mention.length)}\n \n );\n })}\n {children}\n
\n );\n}\n",
"type": "registry:component",
"target": "@components/chat/chat-message-text.tsx"
},
+ {
+ "path": "registry/default/chat/chat-message-timestamp.tsx",
+ "content": "import type { ComponentProps, ReactElement } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport { getChatDateTime } from \"./lib/get-chat-date-time\";\n\nexport type ChatMessageTimestampProps = Omit<\n ComponentProps<\"time\">,\n \"dateTime\"\n> & {\n dateTime: Date | string;\n};\n\nexport function ChatMessageTimestamp({\n dateTime,\n className,\n ...props\n}: ChatMessageTimestampProps): ReactElement {\n return (\n \n );\n}\n",
+ "type": "registry:component",
+ "target": "@components/chat/chat-message-timestamp.tsx"
+ },
{
"path": "registry/default/chat/chat-message.tsx",
- "content": "\"use client\";\n\nimport type { ReactElement, ReactNode } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport { ChatBubbleReactions } from \"./chat-bubble\";\nimport {\n ChatMessageBubble,\n type ChatMessageBubbleProps,\n} from \"./chat-message-bubble\";\nimport {\n ChatMessageAvatar,\n ChatMessageContent,\n ChatMessageFooter,\n ChatMessageHeader,\n ChatMessageLayout,\n type ChatMessageLayoutProps,\n} from \"./chat-message-layout\";\nimport { ChatMessageText } from \"./chat-message-text\";\n\ntype ChatMessageAnimationProps = Pick<\n ChatMessageBubbleProps,\n \"animateEntrance\" | \"highlighted\" | \"onAnimationComplete\"\n>;\n\nexport type ChatMessageProps = Omit<\n ChatMessageLayoutProps,\n \"align\" | \"children\"\n> &\n ChatMessageAnimationProps & {\n side: \"incoming\" | \"outgoing\";\n author?: ReactNode;\n avatar?: ReactNode;\n grouped?: boolean;\n lifted?: boolean;\n children: ReactNode;\n actions?: ReactNode;\n reactions?: ReactNode;\n footer?: ReactNode;\n bubble?: boolean;\n bubbleProps?: Omit<\n ChatMessageBubbleProps,\n \"side\" | \"firstInGroup\" | \"children\" | keyof ChatMessageAnimationProps\n >;\n };\n\nexport function ChatMessage({\n side,\n author,\n avatar,\n grouped = false,\n lifted = false,\n children,\n actions,\n reactions,\n footer,\n bubble = true,\n bubbleProps,\n animateEntrance,\n highlighted,\n onAnimationComplete,\n ...props\n}: ChatMessageProps): ReactElement {\n const own = side === \"outgoing\";\n return (\n \n {!own && !grouped && author && (\n \n {author}\n \n )}\n {!own && avatar && (\n \n {!grouped && avatar}\n \n )}\n \n {bubble ? (\n \n
\n \n {typeof children === \"string\" ? (\n \n ) : (\n children\n )}\n \n
\n {actions}\n
\n ) : (\n children\n )}\n {reactions && {reactions} }\n {footer && {footer} }\n \n \n );\n}\n",
+ "content": "\"use client\";\n\nimport type { ReactElement, ReactNode } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport { ChatBubbleReactions } from \"./chat-bubble\";\nimport {\n ChatMessageBubble,\n type ChatMessageBubbleProps,\n} from \"./chat-message-bubble\";\nimport {\n ChatMessageAvatar,\n ChatMessageContent,\n ChatMessageFooter,\n ChatMessageHeader,\n ChatMessageLayout,\n type ChatMessageLayoutProps,\n} from \"./chat-message-layout\";\nimport { ChatMessageText } from \"./chat-message-text\";\n\ntype ChatMessageAnimationProps = Pick<\n ChatMessageBubbleProps,\n \"animateEntrance\" | \"highlighted\" | \"onAnimationComplete\"\n>;\n\nexport type ChatMessageProps = Omit<\n ChatMessageLayoutProps,\n \"align\" | \"children\"\n> &\n ChatMessageAnimationProps & {\n side: \"incoming\" | \"outgoing\";\n author?: ReactNode;\n avatar?: ReactNode;\n grouped?: boolean;\n lifted?: boolean;\n children: ReactNode;\n actions?: ReactNode;\n reactions?: ReactNode;\n footer?: ReactNode;\n streaming?: boolean;\n bubble?: boolean;\n bubbleProps?: Omit<\n ChatMessageBubbleProps,\n \"side\" | \"firstInGroup\" | \"children\" | keyof ChatMessageAnimationProps\n >;\n };\n\nexport function ChatMessage({\n side,\n author,\n avatar,\n grouped = false,\n lifted = false,\n children,\n actions,\n reactions,\n footer,\n streaming,\n bubble = true,\n bubbleProps,\n animateEntrance,\n highlighted,\n onAnimationComplete,\n ...props\n}: ChatMessageProps): ReactElement {\n const own = side === \"outgoing\";\n return (\n \n {!own && !grouped && author && (\n \n {author}\n \n )}\n {!own && avatar && (\n \n {!grouped && avatar}\n \n )}\n \n {bubble ? (\n \n
\n \n {typeof children === \"string\" ? (\n \n ) : (\n children\n )}\n \n
\n {actions}\n
\n ) : (\n children\n )}\n {reactions && {reactions} }\n {footer && {footer} }\n \n \n );\n}\n",
"type": "registry:component",
"target": "@components/chat/chat-message.tsx"
},
@@ -153,6 +171,12 @@
"type": "registry:component",
"target": "@components/chat/hooks/use-chat-scroll.ts"
},
+ {
+ "path": "registry/default/chat/lib/get-chat-date-time.ts",
+ "content": "export function getChatDateTime(value: Date | string): string | undefined {\n if (typeof value === \"string\") return value;\n if (Number.isNaN(value.getTime())) return undefined;\n return value.toISOString();\n}\n",
+ "type": "registry:component",
+ "target": "@components/chat/lib/get-chat-date-time.ts"
+ },
{
"path": "registry/default/chat/lib/split-chat-message-preview.ts",
"content": "export function splitChatMessagePreview(\n text: string,\n maxPreviewLength = 500,\n): { preview: string; remainder: string | null } {\n if (text.length <= maxPreviewLength)\n return { preview: text, remainder: null };\n\n const limit = Math.max(1, Math.floor(maxPreviewLength));\n const paragraphBreak = text.lastIndexOf(\"\\n\\n\", limit);\n let splitAt = limit;\n if (paragraphBreak > limit / 2) {\n splitAt = paragraphBreak;\n } else {\n for (let index = limit; index > 0; index--) {\n if (/\\s/u.test(text[index] ?? \"\")) {\n splitAt = index;\n break;\n }\n }\n }\n\n if (typeof Intl.Segmenter === \"function\") {\n const segments = new Intl.Segmenter(undefined, {\n granularity: \"grapheme\",\n }).segment(text);\n for (const { index, segment } of segments) {\n if (index + segment.length > splitAt) {\n splitAt = index || segment.length;\n break;\n }\n }\n } else if (/^[\\uDC00-\\uDFFF]$/u.test(text[splitAt] ?? \"\")) {\n splitAt = splitAt === 1 ? 2 : splitAt - 1;\n }\n\n return {\n preview: text.slice(0, splitAt).trimEnd(),\n remainder: text.slice(splitAt).trimStart() || null,\n };\n}\n",
diff --git a/apps/ui/public/r/p-chat-1.json b/apps/ui/public/r/p-chat-1.json
new file mode 100644
index 000000000..d3e1db767
--- /dev/null
+++ b/apps/ui/public/r/p-chat-1.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "p-chat-1",
+ "description": "Chat messages with machine-readable timestamps in the footer",
+ "registryDependencies": [
+ "@coss/chat"
+ ],
+ "files": [
+ {
+ "path": "registry/default/particles/p-chat-1.tsx",
+ "content": "import { ChatMessage } from \"@/registry/default/chat/chat-message\";\nimport { ChatMessageTimestamp } from \"@/registry/default/chat/chat-message-timestamp\";\n\nexport default function Particle() {\n return (\n \n \n 09:41\n \n }\n >\n Can we move the design review to Thursday?\n \n \n 09:43\n \n }\n >\n Thursday works. I’ll send the new invite.\n \n
\n );\n}\n",
+ "type": "registry:block"
+ }
+ ],
+ "meta": {
+ "className": "**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-sm"
+ },
+ "categories": [
+ "chat",
+ "time"
+ ],
+ "type": "registry:block"
+}
\ No newline at end of file
diff --git a/apps/ui/public/r/p-chat-2.json b/apps/ui/public/r/p-chat-2.json
new file mode 100644
index 000000000..647e5ce0c
--- /dev/null
+++ b/apps/ui/public/r/p-chat-2.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "p-chat-2",
+ "description": "Assistant reply with styled Markdown headings, lists, tables, and code",
+ "registryDependencies": [
+ "@coss/chat"
+ ],
+ "files": [
+ {
+ "path": "registry/default/particles/p-chat-2.tsx",
+ "content": "import { ChatMessage } from \"@/registry/default/chat/chat-message\";\nimport { ChatMessageMarkdown } from \"@/registry/default/chat/chat-message-markdown\";\n\nexport default function Particle() {\n return (\n \n \n Interview schedule \n \n The earliest window that works for every interviewer is{\" \"}\n Tuesday at 10:30 . Check the{\" \"}\n shared availability before you send\n invites.\n
\n \n Confirm each interviewer’s calendar \n \n Send booking links with a 15m buffer\n \n Hold a short debrief after each call \n \n \n \n \n Team \n Open slots \n \n \n \n \n Design \n 4 \n \n \n Engineering \n 7 \n \n \n
\n \n {\"buffer: 15\\nminimumNotice: 24h\"}\n \n \n Keep Tuesday morning free for focus time.
\n \n \n \n );\n}\n",
+ "type": "registry:block"
+ }
+ ],
+ "meta": {
+ "className": "**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-md"
+ },
+ "categories": [
+ "chat",
+ "text editor"
+ ],
+ "type": "registry:block"
+}
\ No newline at end of file
diff --git a/apps/ui/public/r/p-chat-3.json b/apps/ui/public/r/p-chat-3.json
new file mode 100644
index 000000000..b752d79ee
--- /dev/null
+++ b/apps/ui/public/r/p-chat-3.json
@@ -0,0 +1,27 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "p-chat-3",
+ "description": "Streaming reply with a trailing caret that never shifts layout",
+ "dependencies": [
+ "lucide-react"
+ ],
+ "registryDependencies": [
+ "@coss/button",
+ "@coss/chat"
+ ],
+ "files": [
+ {
+ "path": "registry/default/particles/p-chat-3.tsx",
+ "content": "\"use client\";\n\nimport { RotateCcwIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { ChatMessage } from \"@/registry/default/chat/chat-message\";\nimport { ChatMessageCaret } from \"@/registry/default/chat/chat-message-caret\";\nimport { ChatMessageText } from \"@/registry/default/chat/chat-message-text\";\nimport { Button } from \"@/registry/default/ui/button\";\n\nconst reply =\n \"Tuesday at 10:30 works for every interviewer. I’ve held the slot and added a 15-minute buffer before the next call.\";\nconst chunks = reply.match(/\\S+\\s*/g) ?? [];\n\nexport default function Particle() {\n const [count, setCount] = useState(0);\n const streaming = count < chunks.length;\n\n useEffect(() => {\n if (count >= chunks.length) return;\n const timeout = window.setTimeout(() => setCount(count + 1), 90);\n return () => window.clearTimeout(timeout);\n }, [count]);\n\n return (\n \n \n \n {streaming && }\n \n \n setCount(0)}\n >\n \n Replay\n \n
\n );\n}\n",
+ "type": "registry:block"
+ }
+ ],
+ "meta": {
+ "className": "**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-sm"
+ },
+ "categories": [
+ "chat",
+ "loading"
+ ],
+ "type": "registry:block"
+}
\ No newline at end of file
diff --git a/apps/ui/public/r/registry.json b/apps/ui/public/r/registry.json
index d72ba6d71..bd3fe5e4f 100644
--- a/apps/ui/public/r/registry.json
+++ b/apps/ui/public/r/registry.json
@@ -1058,6 +1058,11 @@
"type": "registry:component",
"target": "@components/chat/chat-message-bubble.tsx"
},
+ {
+ "path": "registry/default/chat/chat-message-caret.tsx",
+ "type": "registry:component",
+ "target": "@components/chat/chat-message-caret.tsx"
+ },
{
"path": "registry/default/chat/chat-message-info.tsx",
"type": "registry:component",
@@ -1068,11 +1073,21 @@
"type": "registry:component",
"target": "@components/chat/chat-message-layout.tsx"
},
+ {
+ "path": "registry/default/chat/chat-message-markdown.tsx",
+ "type": "registry:component",
+ "target": "@components/chat/chat-message-markdown.tsx"
+ },
{
"path": "registry/default/chat/chat-message-text.tsx",
"type": "registry:component",
"target": "@components/chat/chat-message-text.tsx"
},
+ {
+ "path": "registry/default/chat/chat-message-timestamp.tsx",
+ "type": "registry:component",
+ "target": "@components/chat/chat-message-timestamp.tsx"
+ },
{
"path": "registry/default/chat/chat-message.tsx",
"type": "registry:component",
@@ -1123,6 +1138,11 @@
"type": "registry:component",
"target": "@components/chat/hooks/use-chat-scroll.ts"
},
+ {
+ "path": "registry/default/chat/lib/get-chat-date-time.ts",
+ "type": "registry:component",
+ "target": "@components/chat/lib/get-chat-date-time.ts"
+ },
{
"path": "registry/default/chat/lib/split-chat-message-preview.ts",
"type": "registry:component",
@@ -4429,6 +4449,73 @@
],
"type": "registry:block"
},
+ {
+ "categories": [
+ "chat",
+ "time"
+ ],
+ "description": "Chat messages with machine-readable timestamps in the footer",
+ "files": [
+ {
+ "path": "registry/default/particles/p-chat-1.tsx",
+ "type": "registry:block"
+ }
+ ],
+ "meta": {
+ "className": "**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-sm"
+ },
+ "name": "p-chat-1",
+ "registryDependencies": [
+ "@coss/chat"
+ ],
+ "type": "registry:block"
+ },
+ {
+ "categories": [
+ "chat",
+ "text editor"
+ ],
+ "description": "Assistant reply with styled Markdown headings, lists, tables, and code",
+ "files": [
+ {
+ "path": "registry/default/particles/p-chat-2.tsx",
+ "type": "registry:block"
+ }
+ ],
+ "meta": {
+ "className": "**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-md"
+ },
+ "name": "p-chat-2",
+ "registryDependencies": [
+ "@coss/chat"
+ ],
+ "type": "registry:block"
+ },
+ {
+ "categories": [
+ "chat",
+ "loading"
+ ],
+ "dependencies": [
+ "lucide-react"
+ ],
+ "description": "Streaming reply with a trailing caret that never shifts layout",
+ "files": [
+ {
+ "path": "registry/default/particles/p-chat-3.tsx",
+ "type": "registry:block"
+ }
+ ],
+ "meta": {
+ "className": "**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-sm"
+ },
+ "name": "p-chat-3",
+ "registryDependencies": [
+ "@coss/button",
+ "@coss/chat"
+ ],
+ "type": "registry:block"
+ },
{
"categories": [
"checkbox"
diff --git a/apps/ui/registry.json b/apps/ui/registry.json
index d72ba6d71..bd3fe5e4f 100644
--- a/apps/ui/registry.json
+++ b/apps/ui/registry.json
@@ -1058,6 +1058,11 @@
"type": "registry:component",
"target": "@components/chat/chat-message-bubble.tsx"
},
+ {
+ "path": "registry/default/chat/chat-message-caret.tsx",
+ "type": "registry:component",
+ "target": "@components/chat/chat-message-caret.tsx"
+ },
{
"path": "registry/default/chat/chat-message-info.tsx",
"type": "registry:component",
@@ -1068,11 +1073,21 @@
"type": "registry:component",
"target": "@components/chat/chat-message-layout.tsx"
},
+ {
+ "path": "registry/default/chat/chat-message-markdown.tsx",
+ "type": "registry:component",
+ "target": "@components/chat/chat-message-markdown.tsx"
+ },
{
"path": "registry/default/chat/chat-message-text.tsx",
"type": "registry:component",
"target": "@components/chat/chat-message-text.tsx"
},
+ {
+ "path": "registry/default/chat/chat-message-timestamp.tsx",
+ "type": "registry:component",
+ "target": "@components/chat/chat-message-timestamp.tsx"
+ },
{
"path": "registry/default/chat/chat-message.tsx",
"type": "registry:component",
@@ -1123,6 +1138,11 @@
"type": "registry:component",
"target": "@components/chat/hooks/use-chat-scroll.ts"
},
+ {
+ "path": "registry/default/chat/lib/get-chat-date-time.ts",
+ "type": "registry:component",
+ "target": "@components/chat/lib/get-chat-date-time.ts"
+ },
{
"path": "registry/default/chat/lib/split-chat-message-preview.ts",
"type": "registry:component",
@@ -4429,6 +4449,73 @@
],
"type": "registry:block"
},
+ {
+ "categories": [
+ "chat",
+ "time"
+ ],
+ "description": "Chat messages with machine-readable timestamps in the footer",
+ "files": [
+ {
+ "path": "registry/default/particles/p-chat-1.tsx",
+ "type": "registry:block"
+ }
+ ],
+ "meta": {
+ "className": "**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-sm"
+ },
+ "name": "p-chat-1",
+ "registryDependencies": [
+ "@coss/chat"
+ ],
+ "type": "registry:block"
+ },
+ {
+ "categories": [
+ "chat",
+ "text editor"
+ ],
+ "description": "Assistant reply with styled Markdown headings, lists, tables, and code",
+ "files": [
+ {
+ "path": "registry/default/particles/p-chat-2.tsx",
+ "type": "registry:block"
+ }
+ ],
+ "meta": {
+ "className": "**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-md"
+ },
+ "name": "p-chat-2",
+ "registryDependencies": [
+ "@coss/chat"
+ ],
+ "type": "registry:block"
+ },
+ {
+ "categories": [
+ "chat",
+ "loading"
+ ],
+ "dependencies": [
+ "lucide-react"
+ ],
+ "description": "Streaming reply with a trailing caret that never shifts layout",
+ "files": [
+ {
+ "path": "registry/default/particles/p-chat-3.tsx",
+ "type": "registry:block"
+ }
+ ],
+ "meta": {
+ "className": "**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-sm"
+ },
+ "name": "p-chat-3",
+ "registryDependencies": [
+ "@coss/button",
+ "@coss/chat"
+ ],
+ "type": "registry:block"
+ },
{
"categories": [
"checkbox"
diff --git a/apps/ui/registry/__index__.tsx b/apps/ui/registry/__index__.tsx
index 6836ed672..224728ee6 100644
--- a/apps/ui/registry/__index__.tsx
+++ b/apps/ui/registry/__index__.tsx
@@ -1038,6 +1038,10 @@ export const Index: Record = {
path: "registry/default/chat/chat-message-bubble.tsx",
type: "registry:component",
target: "@components/chat/chat-message-bubble.tsx"
+ },{
+ path: "registry/default/chat/chat-message-caret.tsx",
+ type: "registry:component",
+ target: "@components/chat/chat-message-caret.tsx"
},{
path: "registry/default/chat/chat-message-info.tsx",
type: "registry:component",
@@ -1046,10 +1050,18 @@ export const Index: Record = {
path: "registry/default/chat/chat-message-layout.tsx",
type: "registry:component",
target: "@components/chat/chat-message-layout.tsx"
+ },{
+ path: "registry/default/chat/chat-message-markdown.tsx",
+ type: "registry:component",
+ target: "@components/chat/chat-message-markdown.tsx"
},{
path: "registry/default/chat/chat-message-text.tsx",
type: "registry:component",
target: "@components/chat/chat-message-text.tsx"
+ },{
+ path: "registry/default/chat/chat-message-timestamp.tsx",
+ type: "registry:component",
+ target: "@components/chat/chat-message-timestamp.tsx"
},{
path: "registry/default/chat/chat-message.tsx",
type: "registry:component",
@@ -1090,6 +1102,10 @@ export const Index: Record = {
path: "registry/default/chat/hooks/use-chat-scroll.ts",
type: "registry:component",
target: "@components/chat/hooks/use-chat-scroll.ts"
+ },{
+ path: "registry/default/chat/lib/get-chat-date-time.ts",
+ type: "registry:component",
+ target: "@components/chat/lib/get-chat-date-time.ts"
},{
path: "registry/default/chat/lib/split-chat-message-preview.ts",
type: "registry:component",
@@ -3939,6 +3955,60 @@ export const Index: Record = {
categories: ["card","frame"],
meta: {"className":"**:data-[slot=preview]:w-full **:data-[slot=preview]:flex **:data-[slot=preview]:justify-center"},
},
+ "p-chat-1": {
+ name: "p-chat-1",
+ description: "Chat messages with machine-readable timestamps in the footer",
+ type: "registry:block",
+ registryDependencies: ["@coss/chat"],
+ files: [{
+ path: "registry/default/particles/p-chat-1.tsx",
+ type: "registry:block",
+ target: ""
+ }],
+ component: React.lazy(async () => {
+ const mod = await import("@/registry/default/particles/p-chat-1.tsx")
+ const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
+ return { default: mod.default || mod[exportName] }
+ }),
+ categories: ["chat","time"],
+ meta: {"className":"**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-sm"},
+ },
+ "p-chat-2": {
+ name: "p-chat-2",
+ description: "Assistant reply with styled Markdown headings, lists, tables, and code",
+ type: "registry:block",
+ registryDependencies: ["@coss/chat"],
+ files: [{
+ path: "registry/default/particles/p-chat-2.tsx",
+ type: "registry:block",
+ target: ""
+ }],
+ component: React.lazy(async () => {
+ const mod = await import("@/registry/default/particles/p-chat-2.tsx")
+ const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
+ return { default: mod.default || mod[exportName] }
+ }),
+ categories: ["chat","text editor"],
+ meta: {"className":"**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-md"},
+ },
+ "p-chat-3": {
+ name: "p-chat-3",
+ description: "Streaming reply with a trailing caret that never shifts layout",
+ type: "registry:block",
+ registryDependencies: ["@coss/button","@coss/chat"],
+ files: [{
+ path: "registry/default/particles/p-chat-3.tsx",
+ type: "registry:block",
+ target: ""
+ }],
+ component: React.lazy(async () => {
+ const mod = await import("@/registry/default/particles/p-chat-3.tsx")
+ const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
+ return { default: mod.default || mod[exportName] }
+ }),
+ categories: ["chat","loading"],
+ meta: {"className":"**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-sm"},
+ },
"p-checkbox-1": {
name: "p-checkbox-1",
description: "Basic checkbox",
diff --git a/apps/ui/registry/default/chat/chat-message-caret.tsx b/apps/ui/registry/default/chat/chat-message-caret.tsx
new file mode 100644
index 000000000..4ca07b81a
--- /dev/null
+++ b/apps/ui/registry/default/chat/chat-message-caret.tsx
@@ -0,0 +1,20 @@
+import type { ComponentProps, ReactElement } from "react";
+import { cn } from "@/registry/default/lib/utils";
+
+export function ChatMessageCaret({
+ className,
+ ...props
+}: Omit, "children">): ReactElement {
+ return (
+
+ );
+}
diff --git a/apps/ui/registry/default/chat/chat-message-layout.tsx b/apps/ui/registry/default/chat/chat-message-layout.tsx
index ea464f753..add721748 100644
--- a/apps/ui/registry/default/chat/chat-message-layout.tsx
+++ b/apps/ui/registry/default/chat/chat-message-layout.tsx
@@ -60,13 +60,19 @@ export function ChatMessageHeader({
);
}
+export type ChatMessageContentProps = ComponentProps<"div"> & {
+ streaming?: boolean;
+};
+
export function ChatMessageContent({
+ streaming = false,
className,
...props
-}: ComponentProps<"div">): ReactElement {
+}: ChatMessageContentProps): ReactElement {
return (
[data-slot=chat-message-avatar])]:col-start-2 row-start-2 flex min-w-0 flex-col gap-1 group-data-[align=end]/message:items-end",
className,
diff --git a/apps/ui/registry/default/chat/chat-message-markdown.tsx b/apps/ui/registry/default/chat/chat-message-markdown.tsx
new file mode 100644
index 000000000..548f6c3df
--- /dev/null
+++ b/apps/ui/registry/default/chat/chat-message-markdown.tsx
@@ -0,0 +1,24 @@
+import type { ComponentProps, ReactElement } from "react";
+import { cn } from "@/registry/default/lib/utils";
+
+export function ChatMessageMarkdown({
+ className,
+ ...props
+}: ComponentProps<"div">): ReactElement {
+ return (
+
:first-child]:mt-0 [&>:last-child]:mb-0",
+ "[&_:is(h1,h2,h3,h4,h5,h6)]:mt-4 [&_:is(h1,h2,h3,h4,h5,h6)]:mb-2 [&_:is(h1,h2,h3,h4,h5,h6)]:font-semibold [&_:is(h1,h2,h3,h4,h5,h6)]:leading-snug [&_:is(p,ul,ol,pre,blockquote,table)]:my-3 [&_h1]:text-lg [&_h2]:text-base [&_h6]:text-muted-foreground",
+ "[&_a:hover]:decoration-foreground [&_a]:underline [&_a]:decoration-foreground/32 [&_a]:underline-offset-2 [&_hr]:my-4 [&_strong]:font-semibold",
+ "[&_:is(ul,ol)]:ps-5 [&_li+li]:mt-1 [&_li>:is(ul,ol)]:mt-1 [&_li>:is(ul,ol)]:mb-0 [&_li>p]:my-0 [&_li]:marker:text-muted-foreground [&_ol>li]:marker:tabular-nums [&_ol]:list-decimal [&_ul]:list-disc",
+ "[&_:not(pre)>code]:rounded-sm [&_:not(pre)>code]:bg-foreground/8 [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5 [&_code]:font-mono [&_code]:text-[.875em] [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:border [&_pre]:bg-foreground/4 [&_pre]:px-3 [&_pre]:py-2.5 [&_pre]:leading-normal",
+ "[&_blockquote]:border-s-2 [&_blockquote]:ps-3 [&_blockquote]:text-muted-foreground",
+ "has-[>table]:overflow-x-auto [&_:has(>table)]:overflow-x-auto [&_:is(th,td):first-child]:ps-0 [&_:is(th,td):last-child]:pe-0 [&_:is(th,td)]:px-3 [&_:is(th,td)]:py-1.5 [&_table]:border-collapse [&_tbody_tr:last-child]:border-b-0 [&_td]:align-top [&_td]:tabular-nums [&_th]:text-start [&_th]:align-bottom [&_th]:font-medium [&_tr]:border-b",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
diff --git a/apps/ui/registry/default/chat/chat-message-text.tsx b/apps/ui/registry/default/chat/chat-message-text.tsx
index 48a6037af..9a70f5a84 100644
--- a/apps/ui/registry/default/chat/chat-message-text.tsx
+++ b/apps/ui/registry/default/chat/chat-message-text.tsx
@@ -1,7 +1,7 @@
import type { ComponentProps, ReactElement } from "react";
import { cn } from "@/registry/default/lib/utils";
-export type ChatMessageTextProps = Omit
, "children"> & {
+export type ChatMessageTextProps = ComponentProps<"p"> & {
text: string;
mentions?: readonly string[];
};
@@ -9,6 +9,7 @@ export type ChatMessageTextProps = Omit, "children"> & {
export function ChatMessageText({
text,
mentions = [],
+ children,
className,
...props
}: ChatMessageTextProps): ReactElement {
@@ -16,6 +17,7 @@ export function ChatMessageText({
const parts = text.split(/(@[\p{L}\p{N}_.-]+)/u);
return (
);
})}
+ {children}
);
}
diff --git a/apps/ui/registry/default/chat/chat-message-timestamp.tsx b/apps/ui/registry/default/chat/chat-message-timestamp.tsx
new file mode 100644
index 000000000..a82500364
--- /dev/null
+++ b/apps/ui/registry/default/chat/chat-message-timestamp.tsx
@@ -0,0 +1,25 @@
+import type { ComponentProps, ReactElement } from "react";
+import { cn } from "@/registry/default/lib/utils";
+import { getChatDateTime } from "./lib/get-chat-date-time";
+
+export type ChatMessageTimestampProps = Omit<
+ ComponentProps<"time">,
+ "dateTime"
+> & {
+ dateTime: Date | string;
+};
+
+export function ChatMessageTimestamp({
+ dateTime,
+ className,
+ ...props
+}: ChatMessageTimestampProps): ReactElement {
+ return (
+
+ );
+}
diff --git a/apps/ui/registry/default/chat/chat-message.tsx b/apps/ui/registry/default/chat/chat-message.tsx
index 8c023afe3..b633923d4 100644
--- a/apps/ui/registry/default/chat/chat-message.tsx
+++ b/apps/ui/registry/default/chat/chat-message.tsx
@@ -36,6 +36,7 @@ export type ChatMessageProps = Omit<
actions?: ReactNode;
reactions?: ReactNode;
footer?: ReactNode;
+ streaming?: boolean;
bubble?: boolean;
bubbleProps?: Omit<
ChatMessageBubbleProps,
@@ -53,6 +54,7 @@ export function ChatMessage({
actions,
reactions,
footer,
+ streaming,
bubble = true,
bubbleProps,
animateEntrance,
@@ -73,7 +75,7 @@ export function ChatMessage({
{!grouped && avatar}
)}
-
+
{bubble ? (
+
+ 09:41
+
+ }
+ >
+ Can we move the design review to Thursday?
+
+
+ 09:43
+
+ }
+ >
+ Thursday works. I’ll send the new invite.
+
+
+ );
+}
diff --git a/apps/ui/registry/default/particles/p-chat-2.tsx b/apps/ui/registry/default/particles/p-chat-2.tsx
new file mode 100644
index 000000000..b48222842
--- /dev/null
+++ b/apps/ui/registry/default/particles/p-chat-2.tsx
@@ -0,0 +1,54 @@
+import { ChatMessage } from "@/registry/default/chat/chat-message";
+import { ChatMessageMarkdown } from "@/registry/default/chat/chat-message-markdown";
+
+export default function Particle() {
+ return (
+
+
+ Interview schedule
+
+ The earliest window that works for every interviewer is{" "}
+ Tuesday at 10:30 . Check the{" "}
+ shared availability before you send
+ invites.
+
+
+ Confirm each interviewer’s calendar
+
+ Send booking links with a 15m buffer
+
+ Hold a short debrief after each call
+
+
+
+
+ Team
+ Open slots
+
+
+
+
+ Design
+ 4
+
+
+ Engineering
+ 7
+
+
+
+
+ {"buffer: 15\nminimumNotice: 24h"}
+
+
+ Keep Tuesday morning free for focus time.
+
+
+
+ );
+}
diff --git a/apps/ui/registry/default/particles/p-chat-3.tsx b/apps/ui/registry/default/particles/p-chat-3.tsx
new file mode 100644
index 000000000..515159d57
--- /dev/null
+++ b/apps/ui/registry/default/particles/p-chat-3.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import { RotateCcwIcon } from "lucide-react";
+import { useEffect, useState } from "react";
+import { ChatMessage } from "@/registry/default/chat/chat-message";
+import { ChatMessageCaret } from "@/registry/default/chat/chat-message-caret";
+import { ChatMessageText } from "@/registry/default/chat/chat-message-text";
+import { Button } from "@/registry/default/ui/button";
+
+const reply =
+ "Tuesday at 10:30 works for every interviewer. I’ve held the slot and added a 15-minute buffer before the next call.";
+const chunks = reply.match(/\S+\s*/g) ?? [];
+
+export default function Particle() {
+ const [count, setCount] = useState(0);
+ const streaming = count < chunks.length;
+
+ useEffect(() => {
+ if (count >= chunks.length) return;
+ const timeout = window.setTimeout(() => setCount(count + 1), 90);
+ return () => window.clearTimeout(timeout);
+ }, [count]);
+
+ return (
+
+
+
+ {streaming && }
+
+
+ setCount(0)}
+ >
+
+ Replay
+
+
+ );
+}
diff --git a/apps/ui/registry/registry-categories.ts b/apps/ui/registry/registry-categories.ts
index 8cd8eefb3..74c2fce65 100644
--- a/apps/ui/registry/registry-categories.ts
+++ b/apps/ui/registry/registry-categories.ts
@@ -18,6 +18,7 @@ export const registryCategories = [
"button",
"calendar",
"card",
+ "chat",
"checkbox",
"checkbox group",
"collapsible",
diff --git a/apps/ui/registry/registry-chat.ts b/apps/ui/registry/registry-chat.ts
index 4c614582a..c43589b27 100644
--- a/apps/ui/registry/registry-chat.ts
+++ b/apps/ui/registry/registry-chat.ts
@@ -66,6 +66,11 @@ export const chat: Registry["items"] = [
type: "registry:component",
target: "@components/chat/chat-message-bubble.tsx",
},
+ {
+ path: "chat/chat-message-caret.tsx",
+ type: "registry:component",
+ target: "@components/chat/chat-message-caret.tsx",
+ },
{
path: "chat/chat-message-info.tsx",
type: "registry:component",
@@ -76,11 +81,21 @@ export const chat: Registry["items"] = [
type: "registry:component",
target: "@components/chat/chat-message-layout.tsx",
},
+ {
+ path: "chat/chat-message-markdown.tsx",
+ type: "registry:component",
+ target: "@components/chat/chat-message-markdown.tsx",
+ },
{
path: "chat/chat-message-text.tsx",
type: "registry:component",
target: "@components/chat/chat-message-text.tsx",
},
+ {
+ path: "chat/chat-message-timestamp.tsx",
+ type: "registry:component",
+ target: "@components/chat/chat-message-timestamp.tsx",
+ },
{
path: "chat/chat-message.tsx",
type: "registry:component",
@@ -131,6 +146,11 @@ export const chat: Registry["items"] = [
type: "registry:component",
target: "@components/chat/hooks/use-chat-scroll.ts",
},
+ {
+ path: "chat/lib/get-chat-date-time.ts",
+ type: "registry:component",
+ target: "@components/chat/lib/get-chat-date-time.ts",
+ },
{
path: "chat/lib/split-chat-message-preview.ts",
type: "registry:component",
diff --git a/apps/ui/registry/registry-particles.ts b/apps/ui/registry/registry-particles.ts
index bf00d6f02..a67c903c0 100644
--- a/apps/ui/registry/registry-particles.ts
+++ b/apps/ui/registry/registry-particles.ts
@@ -1679,6 +1679,45 @@ export const particles: ParticleItem[] = [
registryDependencies: ["@coss/button", "@coss/card", "@coss/empty"],
type: "registry:block",
},
+ {
+ categories: categories("chat", "time"),
+ description: "Chat messages with machine-readable timestamps in the footer",
+ files: [{ path: "particles/p-chat-1.tsx", type: "registry:block" }],
+ meta: {
+ className:
+ "**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-sm",
+ },
+ name: "p-chat-1",
+ registryDependencies: ["@coss/chat"],
+ type: "registry:block",
+ },
+ {
+ categories: categories("chat", "text editor"),
+ description:
+ "Assistant reply with styled Markdown headings, lists, tables, and code",
+ files: [{ path: "particles/p-chat-2.tsx", type: "registry:block" }],
+ meta: {
+ className:
+ "**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-md",
+ },
+ name: "p-chat-2",
+ registryDependencies: ["@coss/chat"],
+ type: "registry:block",
+ },
+ {
+ categories: categories("chat", "loading"),
+ dependencies: ["lucide-react"],
+ description:
+ "Streaming reply with a trailing caret that never shifts layout",
+ files: [{ path: "particles/p-chat-3.tsx", type: "registry:block" }],
+ meta: {
+ className:
+ "**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-sm",
+ },
+ name: "p-chat-3",
+ registryDependencies: ["@coss/button", "@coss/chat"],
+ type: "registry:block",
+ },
{
categories: categories("checkbox"),
description: "Basic checkbox",
diff --git a/apps/ui/scripts/validate-registry-deps.mts b/apps/ui/scripts/validate-registry-deps.mts
index d3294ea56..4676b3a8c 100644
--- a/apps/ui/scripts/validate-registry-deps.mts
+++ b/apps/ui/scripts/validate-registry-deps.mts
@@ -84,6 +84,12 @@ function parseImports(content: string): {
continue;
}
+ // Check if it's a chat family import (@/registry/default/chat/*)
+ if (/^@\/registry\/default\/chat\//.test(importPath)) {
+ registryDeps.add(getRegistryPackageName("chat"));
+ continue;
+ }
+
// Check if it's already a @coss import
if (importPath.startsWith("@coss/")) {
// Skip @coss/utils as it's a common utility that shouldn't be treated as a dependency
diff --git a/apps/ui/tests/chat/get-chat-date-time.test.ts b/apps/ui/tests/chat/get-chat-date-time.test.ts
new file mode 100644
index 000000000..f12fdbe9f
--- /dev/null
+++ b/apps/ui/tests/chat/get-chat-date-time.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it } from "bun:test";
+import { getChatDateTime } from "../../registry/default/chat/lib/get-chat-date-time";
+
+describe("chat timestamp machine value", () => {
+ it("serializes a Date as an ISO 8601 instant", () => {
+ expect(getChatDateTime(new Date(Date.UTC(2026, 9, 17, 8, 42)))).toBe(
+ "2026-10-17T08:42:00.000Z",
+ );
+ });
+
+ it.each([
+ "2026-10-17",
+ "2026-10-17T10:42+02:00",
+ "PT2H30M",
+ ])("passes the caller’s %s string through unchanged", (value) => {
+ expect(getChatDateTime(value)).toBe(value);
+ });
+
+ it("omits the machine value for an invalid Date instead of throwing", () => {
+ expect(getChatDateTime(new Date("not a date"))).toBeUndefined();
+ });
+});
diff --git a/packages/ui/src/chat/chat-message-caret.tsx b/packages/ui/src/chat/chat-message-caret.tsx
new file mode 100644
index 000000000..ef25cf90a
--- /dev/null
+++ b/packages/ui/src/chat/chat-message-caret.tsx
@@ -0,0 +1,20 @@
+import { cn } from "@coss/ui/lib/utils";
+import type { ComponentProps, ReactElement } from "react";
+
+export function ChatMessageCaret({
+ className,
+ ...props
+}: Omit, "children">): ReactElement {
+ return (
+
+ );
+}
diff --git a/packages/ui/src/chat/chat-message-layout.tsx b/packages/ui/src/chat/chat-message-layout.tsx
index d17c25b69..924b982ae 100644
--- a/packages/ui/src/chat/chat-message-layout.tsx
+++ b/packages/ui/src/chat/chat-message-layout.tsx
@@ -60,13 +60,19 @@ export function ChatMessageHeader({
);
}
+export type ChatMessageContentProps = ComponentProps<"div"> & {
+ streaming?: boolean;
+};
+
export function ChatMessageContent({
+ streaming = false,
className,
...props
-}: ComponentProps<"div">): ReactElement {
+}: ChatMessageContentProps): ReactElement {
return (
[data-slot=chat-message-avatar])]:col-start-2 row-start-2 flex min-w-0 flex-col gap-1 group-data-[align=end]/message:items-end",
className,
diff --git a/packages/ui/src/chat/chat-message-markdown.tsx b/packages/ui/src/chat/chat-message-markdown.tsx
new file mode 100644
index 000000000..268660eac
--- /dev/null
+++ b/packages/ui/src/chat/chat-message-markdown.tsx
@@ -0,0 +1,24 @@
+import { cn } from "@coss/ui/lib/utils";
+import type { ComponentProps, ReactElement } from "react";
+
+export function ChatMessageMarkdown({
+ className,
+ ...props
+}: ComponentProps<"div">): ReactElement {
+ return (
+
:first-child]:mt-0 [&>:last-child]:mb-0",
+ "[&_:is(h1,h2,h3,h4,h5,h6)]:mt-4 [&_:is(h1,h2,h3,h4,h5,h6)]:mb-2 [&_:is(h1,h2,h3,h4,h5,h6)]:font-semibold [&_:is(h1,h2,h3,h4,h5,h6)]:leading-snug [&_:is(p,ul,ol,pre,blockquote,table)]:my-3 [&_h1]:text-lg [&_h2]:text-base [&_h6]:text-muted-foreground",
+ "[&_a:hover]:decoration-foreground [&_a]:underline [&_a]:decoration-foreground/32 [&_a]:underline-offset-2 [&_hr]:my-4 [&_strong]:font-semibold",
+ "[&_:is(ul,ol)]:ps-5 [&_li+li]:mt-1 [&_li>:is(ul,ol)]:mt-1 [&_li>:is(ul,ol)]:mb-0 [&_li>p]:my-0 [&_li]:marker:text-muted-foreground [&_ol>li]:marker:tabular-nums [&_ol]:list-decimal [&_ul]:list-disc",
+ "[&_:not(pre)>code]:rounded-sm [&_:not(pre)>code]:bg-foreground/8 [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5 [&_code]:font-mono [&_code]:text-[.875em] [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:border [&_pre]:bg-foreground/4 [&_pre]:px-3 [&_pre]:py-2.5 [&_pre]:leading-normal",
+ "[&_blockquote]:border-s-2 [&_blockquote]:ps-3 [&_blockquote]:text-muted-foreground",
+ "has-[>table]:overflow-x-auto [&_:has(>table)]:overflow-x-auto [&_:is(th,td):first-child]:ps-0 [&_:is(th,td):last-child]:pe-0 [&_:is(th,td)]:px-3 [&_:is(th,td)]:py-1.5 [&_table]:border-collapse [&_tbody_tr:last-child]:border-b-0 [&_td]:align-top [&_td]:tabular-nums [&_th]:text-start [&_th]:align-bottom [&_th]:font-medium [&_tr]:border-b",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
diff --git a/packages/ui/src/chat/chat-message-text.tsx b/packages/ui/src/chat/chat-message-text.tsx
index 5c17e91dc..f8263679b 100644
--- a/packages/ui/src/chat/chat-message-text.tsx
+++ b/packages/ui/src/chat/chat-message-text.tsx
@@ -1,7 +1,7 @@
import { cn } from "@coss/ui/lib/utils";
import type { ComponentProps, ReactElement } from "react";
-export type ChatMessageTextProps = Omit
, "children"> & {
+export type ChatMessageTextProps = ComponentProps<"p"> & {
text: string;
mentions?: readonly string[];
};
@@ -9,6 +9,7 @@ export type ChatMessageTextProps = Omit, "children"> & {
export function ChatMessageText({
text,
mentions = [],
+ children,
className,
...props
}: ChatMessageTextProps): ReactElement {
@@ -16,6 +17,7 @@ export function ChatMessageText({
const parts = text.split(/(@[\p{L}\p{N}_.-]+)/u);
return (
);
})}
+ {children}
);
}
diff --git a/packages/ui/src/chat/chat-message-timestamp.tsx b/packages/ui/src/chat/chat-message-timestamp.tsx
new file mode 100644
index 000000000..b8394ddcd
--- /dev/null
+++ b/packages/ui/src/chat/chat-message-timestamp.tsx
@@ -0,0 +1,25 @@
+import { cn } from "@coss/ui/lib/utils";
+import type { ComponentProps, ReactElement } from "react";
+import { getChatDateTime } from "./lib/get-chat-date-time";
+
+export type ChatMessageTimestampProps = Omit<
+ ComponentProps<"time">,
+ "dateTime"
+> & {
+ dateTime: Date | string;
+};
+
+export function ChatMessageTimestamp({
+ dateTime,
+ className,
+ ...props
+}: ChatMessageTimestampProps): ReactElement {
+ return (
+
+ );
+}
diff --git a/packages/ui/src/chat/chat-message.tsx b/packages/ui/src/chat/chat-message.tsx
index be06a3686..ca851acd3 100644
--- a/packages/ui/src/chat/chat-message.tsx
+++ b/packages/ui/src/chat/chat-message.tsx
@@ -36,6 +36,7 @@ export type ChatMessageProps = Omit<
actions?: ReactNode;
reactions?: ReactNode;
footer?: ReactNode;
+ streaming?: boolean;
bubble?: boolean;
bubbleProps?: Omit<
ChatMessageBubbleProps,
@@ -53,6 +54,7 @@ export function ChatMessage({
actions,
reactions,
footer,
+ streaming,
bubble = true,
bubbleProps,
animateEntrance,
@@ -73,7 +75,7 @@ export function ChatMessage({
{!grouped && avatar}
)}
-
+
{bubble ? (