File size: 4,925 Bytes
212c959 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | import { useEffect, useRef, useState } from 'react'
import { ArrowDown, Sparkles } from 'lucide-react'
import MessageBubble from './MessageBubbleNext'
function getDisplayName(user) {
const preferred = user?.name || user?.username || 'there'
const rawName = String(preferred).trim()
if (!rawName || rawName === 'there') return 'there'
const withoutDomain = rawName.includes('@') ? rawName.split('@')[0] : rawName
const readable = withoutDomain
.replace(/[._-]+/g, ' ')
.replace(/\d+/g, ' ')
.trim()
const firstName = readable.split(/\s+/).filter(Boolean)[0] || 'there'
return firstName.charAt(0).toUpperCase() + firstName.slice(1)
}
export default function ChatArea({
messages,
loading,
user,
onCopyMessage,
onRegenerate,
onEdit,
onSpeak,
onPreviewFile,
speakingMessageId,
}) {
const scrollRef = useRef(null)
const autoScrollRef = useRef(true)
const previousMessageCountRef = useRef(messages.length)
const [showScrollButton, setShowScrollButton] = useState(false)
const displayName = getDisplayName(user)
const scrollToBottom = (behavior = 'smooth') => {
if (!scrollRef.current) return
scrollRef.current.scrollTo({
top: scrollRef.current.scrollHeight,
behavior,
})
}
const handleScroll = () => {
if (!scrollRef.current) return
const distanceFromBottom =
scrollRef.current.scrollHeight - scrollRef.current.scrollTop - scrollRef.current.clientHeight
const isNearBottom = distanceFromBottom < 120
autoScrollRef.current = isNearBottom
setShowScrollButton(!isNearBottom && messages.length > 0)
}
useEffect(() => {
if (!messages.length) return
const lastMessage = messages[messages.length - 1]
const nextBehavior =
previousMessageCountRef.current < messages.length && !lastMessage?.streaming ? 'smooth' : 'auto'
if (autoScrollRef.current || lastMessage?.role === 'user' || lastMessage?.streaming) {
const frame = window.requestAnimationFrame(() => {
scrollToBottom(nextBehavior)
})
previousMessageCountRef.current = messages.length
return () => window.cancelAnimationFrame(frame)
}
previousMessageCountRef.current = messages.length
}, [messages])
if (!messages.length && loading) {
return (
<div className="rich-scroll min-h-0 flex-1 overflow-y-auto px-4 py-8 sm:px-6">
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4">
{Array.from({ length: 4 }).map((_, index) => (
<div
key={index}
className={`animate-pulse rounded-lg bg-secondary p-4 ${
index % 2 === 0 ? 'mr-auto w-10/12' : 'ml-auto w-7/12'
}`}
>
<div className="space-y-3">
<div className="h-3 rounded-full bg-background/80" />
<div className="h-3 w-11/12 rounded-full bg-background/80" />
<div className="h-3 w-8/12 rounded-full bg-background/80" />
</div>
</div>
))}
</div>
</div>
)
}
if (!messages.length && !loading) {
return (
<div className="flex min-h-0 flex-1 items-center justify-center overflow-y-auto px-4 py-10 sm:px-6">
<div className="flex flex-col items-center gap-3 text-center">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-secondary text-muted-foreground">
<Sparkles className="h-4 w-4" />
</div>
<h2 className="text-2xl font-semibold text-foreground sm:text-3xl">
Hi {displayName}, what can we make today?
</h2>
</div>
</div>
)
}
return (
<div className="relative min-h-0 flex-1">
<div
ref={scrollRef}
onScroll={handleScroll}
className="rich-scroll h-full min-h-0 overflow-y-auto"
>
<div className="py-4">
{messages.map((message) => (
<MessageBubble
key={message.id}
message={message}
user={user}
onCopyMessage={onCopyMessage}
onRegenerate={onRegenerate}
onEdit={onEdit}
onSpeak={onSpeak}
onPreviewFile={onPreviewFile}
speaking={speakingMessageId === message.id}
/>
))}
</div>
</div>
{showScrollButton ? (
<button
type="button"
onClick={() => {
autoScrollRef.current = true
setShowScrollButton(false)
scrollToBottom('smooth')
}}
className="absolute bottom-5 right-5 inline-flex h-10 w-10 items-center justify-center rounded-lg border border-border bg-card text-foreground shadow-panel transition hover:bg-secondary"
aria-label="Scroll to latest message"
>
<ArrowDown className="h-4 w-4" />
</button>
) : null}
</div>
)
}
|