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>
  )
}