perf(playground): improve chat markdown rendering
- refine assistant and user message surfaces so chat content matches the app UI. - normalize markdown typography, tables, images, lists, blockquotes, and details rendering. - add indentation cues for collapsible reasoning and source sections.
This commit is contained in:
+1
-1
@@ -29,7 +29,7 @@ export type ConversationProps = ComponentProps<typeof StickToBottom>
|
|||||||
|
|
||||||
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
||||||
<StickToBottom
|
<StickToBottom
|
||||||
className={cn('relative flex-1 overflow-y-auto', className)}
|
className={cn('relative min-h-0 flex-1 overflow-hidden', className)}
|
||||||
initial='smooth'
|
initial='smooth'
|
||||||
resize='smooth'
|
resize='smooth'
|
||||||
role='log'
|
role='log'
|
||||||
|
|||||||
+1
-1
@@ -188,7 +188,7 @@ export const ReasoningContent = memo(
|
|||||||
({ className, children, ...props }: ReasoningContentProps) => (
|
({ className, children, ...props }: ReasoningContentProps) => (
|
||||||
<CollapsibleContent
|
<CollapsibleContent
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-4 text-sm',
|
'border-border/70 mt-3 ml-2 border-l pl-4 text-sm',
|
||||||
'data-closed:fade-out-0 data-closed:slide-out-to-top-2 data-open:slide-in-from-top-2 text-muted-foreground data-closed:animate-out data-open:animate-in outline-none',
|
'data-closed:fade-out-0 data-closed:slide-out-to-top-2 data-open:slide-in-from-top-2 text-muted-foreground data-closed:animate-out data-open:animate-in outline-none',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
+371
-2
@@ -18,7 +18,15 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { type ComponentProps, memo, type ReactNode } from 'react'
|
import {
|
||||||
|
Children,
|
||||||
|
type ComponentProps,
|
||||||
|
type JSX,
|
||||||
|
isValidElement,
|
||||||
|
memo,
|
||||||
|
type ReactNode,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import { Streamdown, type Components } from 'streamdown'
|
import { Streamdown, type Components } from 'streamdown'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
@@ -33,6 +41,11 @@ type CodeComponentProps = ComponentProps<'code'> & {
|
|||||||
'data-block'?: boolean
|
'data-block'?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MarkdownElementProps<T extends keyof JSX.IntrinsicElements> =
|
||||||
|
ComponentProps<T> & {
|
||||||
|
node?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
function getCodeText(children: ReactNode) {
|
function getCodeText(children: ReactNode) {
|
||||||
if (typeof children === 'string') {
|
if (typeof children === 'string') {
|
||||||
return children.replace(/\n$/, '')
|
return children.replace(/\n$/, '')
|
||||||
@@ -49,7 +62,354 @@ function getCodeLanguage(className?: string) {
|
|||||||
return className?.match(/language-([\w#+.-]+)/)?.[1] ?? 'plaintext'
|
return className?.match(/language-([\w#+.-]+)/)?.[1] ?? 'plaintext'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSummaryElement(child: ReactNode) {
|
||||||
|
return isValidElement(child) && child.type === 'summary'
|
||||||
|
}
|
||||||
|
|
||||||
|
function MarkdownImage({
|
||||||
|
alt,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
src,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'img'>) {
|
||||||
|
const [hasError, setHasError] = useState(false)
|
||||||
|
|
||||||
|
if (!src || hasError) {
|
||||||
|
return (
|
||||||
|
<span className='border-border/70 text-muted-foreground my-4 inline-flex rounded-md border px-3 py-2 text-xs italic'>
|
||||||
|
{alt || 'Image not available'}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
alt={alt}
|
||||||
|
className={cn(
|
||||||
|
'border-border/70 my-4 block h-auto max-h-96 max-w-full rounded-lg border object-contain',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
loading='lazy'
|
||||||
|
onError={() => setHasError(true)}
|
||||||
|
src={src}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const responseComponents: Components = {
|
const responseComponents: Components = {
|
||||||
|
h1({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'h1'>) {
|
||||||
|
return (
|
||||||
|
<h1
|
||||||
|
className={cn(
|
||||||
|
'mt-6 mb-3 text-xl font-semibold tracking-normal',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h1>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
h2({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'h2'>) {
|
||||||
|
return (
|
||||||
|
<h2
|
||||||
|
className={cn(
|
||||||
|
'mt-6 mb-3 text-lg font-semibold tracking-normal',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
h3({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'h3'>) {
|
||||||
|
return (
|
||||||
|
<h3
|
||||||
|
className={cn(
|
||||||
|
'mt-5 mb-2 text-base font-semibold tracking-normal',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h3>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
h4({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'h4'>) {
|
||||||
|
return (
|
||||||
|
<h4
|
||||||
|
className={cn('mt-5 mb-2 text-sm font-semibold', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h4>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
h5({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'h5'>) {
|
||||||
|
return (
|
||||||
|
<h5
|
||||||
|
className={cn(
|
||||||
|
'text-muted-foreground mt-4 mb-2 text-sm font-semibold',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h5>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
h6({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'h6'>) {
|
||||||
|
return (
|
||||||
|
<h6
|
||||||
|
className={cn(
|
||||||
|
'text-muted-foreground mt-4 mb-2 text-xs font-semibold uppercase',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h6>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
ul({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'ul'>) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
className={cn(
|
||||||
|
'my-3 list-outside list-disc space-y-1.5 pl-5',
|
||||||
|
'[&.contains-task-list]:list-none [&.contains-task-list]:pl-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
ol({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'ol'>) {
|
||||||
|
return (
|
||||||
|
<ol
|
||||||
|
className={cn(
|
||||||
|
'my-3 list-outside list-decimal space-y-1.5 pl-5',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ol>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
li({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'li'>) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className={cn(
|
||||||
|
'marker:text-muted-foreground pl-1 leading-7',
|
||||||
|
'[&.task-list-item]:flex [&.task-list-item]:items-start [&.task-list-item]:gap-2 [&.task-list-item]:pl-0',
|
||||||
|
'[&.task-list-item>input]:accent-primary [&.task-list-item>input]:mt-1.5 [&.task-list-item>input]:size-4',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
details({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'details'>) {
|
||||||
|
const childArray = Children.toArray(children)
|
||||||
|
const summaryChildren = childArray.filter(isSummaryElement)
|
||||||
|
const contentChildren = childArray.filter(
|
||||||
|
(child) => !isSummaryElement(child)
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<details className={cn('my-4', className)} {...props}>
|
||||||
|
{summaryChildren}
|
||||||
|
{contentChildren.length > 0 && (
|
||||||
|
<div className='border-border/70 ml-5 border-l pl-4'>
|
||||||
|
{contentChildren}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
summary({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'summary'>) {
|
||||||
|
return (
|
||||||
|
<summary
|
||||||
|
className={cn(
|
||||||
|
'text-foreground marker:text-muted-foreground mb-2 cursor-pointer text-sm font-semibold',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</summary>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
blockquote({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'blockquote'>) {
|
||||||
|
return (
|
||||||
|
<blockquote
|
||||||
|
className={cn(
|
||||||
|
'border-border text-muted-foreground my-4 border-l-2 pl-4',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</blockquote>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
hr({ className, node: _node, ...props }: MarkdownElementProps<'hr'>) {
|
||||||
|
return <hr className={cn('border-border/70 my-6', className)} {...props} />
|
||||||
|
},
|
||||||
|
img: MarkdownImage,
|
||||||
|
table({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'table'>) {
|
||||||
|
return (
|
||||||
|
<div className='border-border/70 my-4 w-full overflow-x-auto rounded-lg border'>
|
||||||
|
<table
|
||||||
|
className={cn(
|
||||||
|
'w-full min-w-max border-separate border-spacing-0 text-sm',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
thead({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'thead'>) {
|
||||||
|
return (
|
||||||
|
<thead className={cn('bg-muted/60', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</thead>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
tbody({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'tbody'>) {
|
||||||
|
return (
|
||||||
|
<tbody className={cn('divide-border/70 divide-y', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</tbody>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
tr({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'tr'>) {
|
||||||
|
return (
|
||||||
|
<tr className={cn('border-border/70', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
th({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'th'>) {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
className={cn(
|
||||||
|
'text-muted-foreground px-3 py-2 text-left text-xs font-semibold whitespace-nowrap',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</th>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
td({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
node: _node,
|
||||||
|
...props
|
||||||
|
}: MarkdownElementProps<'td'>) {
|
||||||
|
return (
|
||||||
|
<td className={cn('px-3 py-2 align-top', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
},
|
||||||
code({ children, className, ...props }: CodeComponentProps) {
|
code({ children, className, ...props }: CodeComponentProps) {
|
||||||
if (!props['data-block']) {
|
if (!props['data-block']) {
|
||||||
return (
|
return (
|
||||||
@@ -107,7 +467,16 @@ export const Response = memo(
|
|||||||
return (
|
return (
|
||||||
<Streamdown
|
<Streamdown
|
||||||
className={cn(
|
className={cn(
|
||||||
'size-full min-w-0 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
|
'size-full min-w-0 text-pretty',
|
||||||
|
'[&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
|
||||||
|
'[&_p]:my-3 [&_p]:leading-7',
|
||||||
|
'[&_strong]:text-foreground [&_strong]:font-semibold',
|
||||||
|
'[&_a]:text-primary [&_a]:underline-offset-4 hover:[&_a]:underline',
|
||||||
|
'[&_details>summary~*]:border-border/70 [&_details]:my-4 [&_details>summary~*]:ml-5 [&_details>summary~*]:border-l [&_details>summary~*]:pl-4',
|
||||||
|
'[&_summary]:text-foreground [&_summary::marker]:text-muted-foreground [&_summary]:mb-2 [&_summary]:cursor-pointer [&_summary]:text-sm [&_summary]:font-semibold',
|
||||||
|
'[&_[data-streamdown=table-wrapper]]:border-0 [&_[data-streamdown=table-wrapper]]:bg-transparent [&_[data-streamdown=table-wrapper]]:p-0 [&_[data-streamdown=table-wrapper]]:shadow-none',
|
||||||
|
'[&_[data-streamdown=table-wrapper]>div:first-child]:hidden',
|
||||||
|
'[&_[data-streamdown=table-wrapper]>div:last-child]:border-border/70 [&_[data-streamdown=table-wrapper]>div:last-child]:rounded-lg',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
components={{ ...responseComponents, ...components }}
|
components={{ ...responseComponents, ...components }}
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ export const SourcesContent = ({
|
|||||||
}: SourcesContentProps) => (
|
}: SourcesContentProps) => (
|
||||||
<CollapsibleContent
|
<CollapsibleContent
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-3 flex w-fit flex-col gap-2',
|
'border-border/70 mt-3 ml-2 flex w-fit flex-col gap-2 border-l pl-4',
|
||||||
'data-closed:fade-out-0 data-closed:slide-out-to-top-2 data-open:slide-in-from-top-2 data-closed:animate-out data-open:animate-in outline-none',
|
'data-closed:fade-out-0 data-closed:slide-out-to-top-2 data-open:slide-in-from-top-2 data-closed:animate-out data-open:animate-in outline-none',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -98,11 +98,15 @@ export function PlaygroundChat({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Message
|
<Message
|
||||||
className='group flex-row-reverse'
|
className={
|
||||||
|
message.from === 'assistant'
|
||||||
|
? 'group flex-row-reverse py-3'
|
||||||
|
: 'group flex-row-reverse py-1.5'
|
||||||
|
}
|
||||||
from={message.from}
|
from={message.from}
|
||||||
key={message.key}
|
key={message.key}
|
||||||
>
|
>
|
||||||
<div className='w-full min-w-0 flex-1 basis-full py-1'>
|
<div className='w-full min-w-0 flex-1 basis-full'>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<PlaygroundMessageEditor
|
<PlaygroundMessageEditor
|
||||||
editText={editText}
|
editText={editText}
|
||||||
@@ -124,7 +128,7 @@ export function PlaygroundChat({
|
|||||||
onDelete={onDeleteMessage}
|
onDelete={onDeleteMessage}
|
||||||
isGenerating={isGenerating}
|
isGenerating={isGenerating}
|
||||||
alwaysVisible={alwaysShowActions}
|
alwaysVisible={alwaysShowActions}
|
||||||
className='mt-1'
|
className='mt-2'
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
message={message}
|
message={message}
|
||||||
|
|||||||
+2
-2
@@ -73,9 +73,9 @@ export function Playground() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative flex size-full flex-col overflow-hidden'>
|
<div className='relative flex size-full min-h-0 flex-col overflow-hidden'>
|
||||||
{/* Full-width scroll container: scrolling works even over side whitespace */}
|
{/* Full-width scroll container: scrolling works even over side whitespace */}
|
||||||
<div className='flex flex-1 flex-col overflow-hidden'>
|
<div className='flex min-h-0 flex-1 flex-col overflow-hidden'>
|
||||||
<PlaygroundChat
|
<PlaygroundChat
|
||||||
messages={messages}
|
messages={messages}
|
||||||
onRegenerateMessage={handleRegenerateMessage}
|
onRegenerateMessage={handleRegenerateMessage}
|
||||||
|
|||||||
+24
-10
@@ -22,25 +22,39 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
export function getMessageContentStyles() {
|
export function getMessageContentStyles() {
|
||||||
return [
|
return [
|
||||||
// Assistant content fills the row; user bubble auto-width
|
// Assistant content reads like a document column; user bubble stays compact.
|
||||||
'group-[.is-assistant]:w-full',
|
'group-[.is-assistant]:w-full',
|
||||||
'group-[.is-assistant]:max-w-none',
|
'group-[.is-assistant]:max-w-[78ch]',
|
||||||
'group-[.is-user]:w-fit',
|
'group-[.is-user]:w-fit',
|
||||||
// User bubble: rounded and themed background
|
|
||||||
|
// User bubble: compact surface that stays calm in both light and dark themes.
|
||||||
|
'group-[.is-user]:rounded-2xl',
|
||||||
|
'group-[.is-user]:rounded-br-md',
|
||||||
|
'group-[.is-user]:border',
|
||||||
|
'group-[.is-user]:border-border/70',
|
||||||
|
'group-[.is-user]:bg-muted/70',
|
||||||
|
'group-[.is-user]:px-4',
|
||||||
|
'group-[.is-user]:py-2.5',
|
||||||
'group-[.is-user]:text-foreground',
|
'group-[.is-user]:text-foreground',
|
||||||
'group-[.is-user]:bg-secondary',
|
'group-[.is-user]:shadow-sm',
|
||||||
'dark:group-[.is-user]:bg-muted',
|
'group-[.is-user]:shadow-black/5',
|
||||||
'group-[.is-user]:rounded-3xl',
|
|
||||||
// Assistant bubble: flat serif style (one-sided style)
|
// Assistant response: flat reading surface using the active UI font axis.
|
||||||
'group-[.is-assistant]:text-foreground',
|
|
||||||
'group-[.is-assistant]:bg-transparent',
|
'group-[.is-assistant]:bg-transparent',
|
||||||
'group-[.is-assistant]:p-0',
|
'group-[.is-assistant]:p-0',
|
||||||
'group-[.is-assistant]:font-serif',
|
'group-[.is-assistant]:rounded-none',
|
||||||
|
'group-[.is-assistant]:overflow-visible',
|
||||||
|
'group-[.is-assistant]:[font-family:var(--font-body)]',
|
||||||
|
'group-[.is-assistant]:text-foreground/90',
|
||||||
|
|
||||||
// Preferred readable widths and wrapping
|
// Preferred readable widths and wrapping
|
||||||
'leading-relaxed',
|
'text-[0.95rem]',
|
||||||
|
'leading-6',
|
||||||
'break-words',
|
'break-words',
|
||||||
'whitespace-pre-wrap',
|
'whitespace-pre-wrap',
|
||||||
|
'sm:text-[0.975rem]',
|
||||||
'sm:leading-7',
|
'sm:leading-7',
|
||||||
|
|
||||||
// Cap user bubble width so it does not look like a banner
|
// Cap user bubble width so it does not look like a banner
|
||||||
'group-[.is-user]:max-w-[85%]',
|
'group-[.is-user]:max-w-[85%]',
|
||||||
'sm:group-[.is-user]:max-w-[62ch]',
|
'sm:group-[.is-user]:max-w-[62ch]',
|
||||||
|
|||||||
Reference in New Issue
Block a user