cc-haha/desktop/src/components/chat/AssistantMessage.tsx
2026-05-01 22:09:55 +08:00

60 lines
2.0 KiB
TypeScript

import { MarkdownRenderer } from '../markdown/MarkdownRenderer'
import { MessageActionBar } from './MessageActionBar'
import { InlineImageGallery } from './InlineImageGallery'
type Props = {
content: string
isStreaming?: boolean
}
export function AssistantMessage({ content, isStreaming }: Props) {
if (!content.trim()) return null
const documentLayout = shouldUseDocumentLayout(content)
return (
<div className="group mb-5 flex justify-start">
<div
data-message-shell="assistant"
data-layout={documentLayout ? 'document' : 'bubble'}
className={`flex min-w-0 flex-col items-start gap-2 ${
documentLayout
? 'w-full max-w-full'
: 'w-full max-w-[88%] sm:max-w-[80%] lg:max-w-[72%]'
}`}
>
<div className={`rounded-[20px] rounded-tl-[8px] border border-[var(--color-border)]/60 bg-[var(--color-surface)] px-4 py-3 text-sm text-[var(--color-text-primary)] shadow-sm ${
documentLayout ? 'w-full' : 'max-w-full'
}`}>
<MarkdownRenderer content={content} variant={documentLayout ? 'document' : 'default'} />
{!isStreaming && <InlineImageGallery text={content} />}
{isStreaming && (
<span className="ml-0.5 inline-block h-4 w-0.5 animate-shimmer bg-[var(--color-brand)] align-text-bottom" />
)}
</div>
<MessageActionBar
copyText={isStreaming ? undefined : content}
copyLabel="Copy reply"
align="start"
/>
</div>
</div>
)
}
function shouldUseDocumentLayout(content: string) {
const normalized = content.trim()
if (!normalized) return false
if (/```/.test(normalized)) return true
if (/^\s{0,3}(#{1,6}\s|[-*+]\s|\d+\.\s|>\s|\|.+\|)/m.test(normalized)) return true
const paragraphs = normalized
.split(/\n\s*\n/)
.map((chunk) => chunk.trim())
.filter(Boolean)
return paragraphs.length >= 2 || normalized.split('\n').filter((line) => line.trim()).length >= 8
}