fix(ui): reasoning display glitches and scroll button persistence (#1206)

This commit is contained in:
josh 2025-09-16 11:41:52 +01:00 committed by GitHub
parent a1844c8294
commit b229241854
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 46 additions and 37 deletions

View file

@ -1,5 +1,6 @@
'use client'; 'use client';
import { useState, useEffect } from 'react';
import { import {
Reasoning, Reasoning,
ReasoningTrigger, ReasoningTrigger,
@ -15,10 +16,18 @@ export function MessageReasoning({
isLoading, isLoading,
reasoning, reasoning,
}: MessageReasoningProps) { }: MessageReasoningProps) {
const [hasBeenStreaming, setHasBeenStreaming] = useState(isLoading);
useEffect(() => {
if (isLoading) {
setHasBeenStreaming(true);
}
}, [isLoading]);
return ( return (
<Reasoning <Reasoning
isStreaming={isLoading} isStreaming={isLoading}
defaultOpen={true} defaultOpen={hasBeenStreaming}
data-testid="message-reasoning" data-testid="message-reasoning"
> >
<ReasoningTrigger /> <ReasoningTrigger />

View file

@ -38,9 +38,6 @@ import { SelectItem } from '@/components/ui/select';
import * as SelectPrimitive from '@radix-ui/react-select'; import * as SelectPrimitive from '@radix-ui/react-select';
import equal from 'fast-deep-equal'; import equal from 'fast-deep-equal';
import type { UseChatHelpers } from '@ai-sdk/react'; import type { UseChatHelpers } from '@ai-sdk/react';
import { AnimatePresence, motion } from 'framer-motion';
import { ArrowDown } from 'lucide-react';
import { useScrollToBottom } from '@/hooks/use-scroll-to-bottom';
import type { VisibilityType } from './visibility-selector'; import type { VisibilityType } from './visibility-selector';
import type { Attachment, ChatMessage } from '@/lib/types'; import type { Attachment, ChatMessage } from '@/lib/types';
import type { AppUsage } from '@/lib/usage'; import type { AppUsage } from '@/lib/usage';
@ -234,40 +231,8 @@ function PureMultimodalInput({
[setAttachments], [setAttachments],
); );
const { isAtBottom, scrollToBottom } = useScrollToBottom();
useEffect(() => {
if (status === 'submitted') {
scrollToBottom();
}
}, [status, scrollToBottom]);
return ( return (
<div className="flex relative flex-col gap-4 w-full"> <div className="flex relative flex-col gap-4 w-full">
<AnimatePresence>
{!isAtBottom && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
className="absolute -top-12 left-1/2 z-50 -translate-x-1/2"
>
<Button
data-testid="scroll-to-bottom-button"
className="rounded-full"
size="icon"
variant="outline"
onClick={(event) => {
event.preventDefault();
scrollToBottom();
}}
>
<ArrowDown />
</Button>
</motion.div>
)}
</AnimatePresence>
{messages.length === 0 && {messages.length === 0 &&
attachments.length === 0 && attachments.length === 0 &&

View file

@ -17,7 +17,42 @@ export function useScrollToBottom() {
// Check if we are within 100px of the bottom (like v0 does) // Check if we are within 100px of the bottom (like v0 does)
setIsAtBottom(scrollTop + clientHeight >= scrollHeight - 100); setIsAtBottom(scrollTop + clientHeight >= scrollHeight - 100);
}, []); }, []);
useEffect(() => {
if (!containerRef.current) return;
const container = containerRef.current;
const resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(() => {
handleScroll();
});
});
const mutationObserver = new MutationObserver(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
handleScroll();
});
});
});
resizeObserver.observe(container);
mutationObserver.observe(container, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class', 'data-state']
});
handleScroll();
return () => {
resizeObserver.disconnect();
mutationObserver.disconnect();
};
}, [handleScroll]);
useEffect(() => { useEffect(() => {
const container = containerRef.current; const container = containerRef.current;