|
270 | 270 | const [messages, setMessages] = useState([]); |
271 | 271 | const [input, setInput] = useState(''); |
272 | 272 | const [isLoading, setIsLoading] = useState(false); |
| 273 | + const [userScrolledUp, setUserScrolledUp] = useState(false); |
| 274 | + const [showScrollButton, setShowScrollButton] = useState(false); |
273 | 275 | const messagesEndRef = useRef(null); |
| 276 | + const messagesContainerRef = useRef(null); |
274 | 277 |
|
275 | 278 | const scrollToBottom = () => { |
276 | 279 | messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); |
| 280 | + setShowScrollButton(false); |
| 281 | + }; |
| 282 | + |
| 283 | + const handleScroll = (e) => { |
| 284 | + const { scrollTop, scrollHeight, clientHeight } = e.target; |
| 285 | + const isAtBottom = scrollHeight - scrollTop <= clientHeight + 10; |
| 286 | + setUserScrolledUp(!isAtBottom); |
277 | 287 | }; |
278 | 288 |
|
279 | 289 | useEffect(() => { |
280 | | - scrollToBottom(); |
281 | | - }, [messages]); |
| 290 | + if (messages.length > 0) { |
| 291 | + const lastMessage = messages[messages.length - 1]; |
| 292 | + if (lastMessage.role === 'user') { |
| 293 | + // User sent message - always scroll to bottom |
| 294 | + scrollToBottom(); |
| 295 | + setShowScrollButton(false); |
| 296 | + } else { |
| 297 | + // AI sent message - check if user scrolled up |
| 298 | + if (userScrolledUp) { |
| 299 | + setShowScrollButton(true); |
| 300 | + } else { |
| 301 | + scrollToBottom(); |
| 302 | + } |
| 303 | + } |
| 304 | + } |
| 305 | + }, [messages, userScrolledUp]); |
282 | 306 |
|
283 | 307 | useEffect(() => { |
284 | 308 | const unsubscribe = wsRuntime.subscribe((data) => { |
285 | 309 | if (data.type === 'status') return; |
286 | | - |
287 | 310 | setMessages(prev => [...prev, { |
288 | 311 | id: data.id || Date.now().toString(), |
289 | 312 | role: data.role || 'assistant', |
290 | 313 | content: data.content |
291 | 314 | }]); |
292 | 315 | setIsLoading(false); |
293 | 316 | }); |
294 | | - |
295 | 317 | return unsubscribe; |
296 | 318 | }, [wsRuntime]); |
297 | 319 |
|
298 | 320 | const handleSubmit = (e) => { |
299 | 321 | e.preventDefault(); |
300 | 322 | if (!input.trim() || isLoading) return; |
301 | | - |
302 | 323 | const userMessage = { |
303 | 324 | id: Date.now().toString(), |
304 | 325 | role: 'user', |
305 | 326 | content: input.trim() |
306 | 327 | }; |
307 | | - |
308 | 328 | setMessages(prev => [...prev, userMessage]); |
309 | 329 | setInput(''); |
310 | 330 | setIsLoading(true); |
311 | | - |
312 | 331 | wsRuntime.send(userMessage.content); |
313 | 332 | }; |
314 | 333 |
|
315 | 334 | return React.createElement('div', { className: 'flex flex-col h-full' }, |
316 | 335 | // Messages area |
317 | | - React.createElement('div', { className: 'flex-1 overflow-y-auto p-4' }, |
| 336 | + React.createElement('div', { |
| 337 | + className: 'flex-1 overflow-y-auto p-4 relative', |
| 338 | + onScroll: handleScroll, |
| 339 | + ref: messagesContainerRef |
| 340 | + }, |
318 | 341 | messages.length === 0 && React.createElement('div', { className: 'text-center text-gray-500 mt-8' }, |
319 | 342 | React.createElement('p', { className: 'text-lg mb-2' }, '👋 Hello! I\'m your Plexe Assistant.'), |
320 | 343 | React.createElement('p', { className: 'text-sm' }, 'I help you build machine learning models through natural conversation.'), |
|
332 | 355 | ), |
333 | 356 | React.createElement('div', { ref: messagesEndRef }) |
334 | 357 | ), |
| 358 | + // Scroll to bottom button |
| 359 | + showScrollButton && React.createElement('button', { |
| 360 | + onClick: scrollToBottom, |
| 361 | + className: 'fixed bottom-20 right-4 bg-blue-500 text-white rounded-full p-3 shadow-lg hover:bg-blue-600 transition-colors z-10' |
| 362 | + }, '↓'), |
335 | 363 | // Input area |
336 | 364 | React.createElement('form', { onSubmit: handleSubmit, className: 'border-t p-4' }, |
337 | 365 | React.createElement('div', { className: 'flex space-x-2' }, |
|
0 commit comments