Prevent forced auto-scroll in playground chat

This commit is contained in:
akhisud3195 2025-07-11 20:34:14 +05:30
parent 931e026cdc
commit 3c2bde91b0
3 changed files with 102 additions and 44 deletions

View file

@ -127,3 +127,23 @@ body {
background: var(--background); background: var(--background);
color: var(--foreground); color: var(--foreground);
} }
/* Playground chat custom scrollbar: hide track background and border */
.playground-scrollbar::-webkit-scrollbar {
width: 4px;
background: transparent !important;
}
.playground-scrollbar::-webkit-scrollbar-track {
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
.playground-scrollbar::-webkit-scrollbar-thumb {
background: #9ca3af;
border-radius: 4px;
}
.playground-scrollbar {
scrollbar-width: thin;
scrollbar-color: #9ca3af transparent;
}

View file

@ -12,6 +12,7 @@ import { WithStringId } from "@/app/lib/types/types";
import { ProfileContextBox } from "./profile-context-box"; import { ProfileContextBox } from "./profile-context-box";
import { USE_TESTING_FEATURE } from "@/app/lib/feature_flags"; import { USE_TESTING_FEATURE } from "@/app/lib/feature_flags";
import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal"; import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
export function Chat({ export function Chat({
chat, chat,
@ -51,6 +52,32 @@ export function Chat({
const [optimisticMessages, setOptimisticMessages] = useState<z.infer<typeof Message>[]>(chat.messages); const [optimisticMessages, setOptimisticMessages] = useState<z.infer<typeof Message>[]>(chat.messages);
const [isLastInteracted, setIsLastInteracted] = useState(false); const [isLastInteracted, setIsLastInteracted] = useState(false);
// --- Scroll/auto-scroll/unread bubble logic ---
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState(true);
const [showUnreadBubble, setShowUnreadBubble] = useState(false);
const handleScroll = useCallback(() => {
const container = scrollContainerRef.current;
if (!container) return;
const { scrollTop, scrollHeight, clientHeight } = container;
const atBottom = scrollHeight - scrollTop - clientHeight < 20;
setAutoScroll(atBottom);
if (atBottom) setShowUnreadBubble(false);
}, []);
useEffect(() => {
const container = scrollContainerRef.current;
if (!container) return;
if (autoScroll) {
container.scrollTop = container.scrollHeight;
setShowUnreadBubble(false);
} else {
setShowUnreadBubble(true);
}
}, [optimisticMessages, loadingAssistantResponse, autoScroll]);
// --- End scroll/auto-scroll logic ---
const getCopyContent = useCallback(() => { const getCopyContent = useCallback(() => {
return JSON.stringify({ return JSON.stringify({
messages: [{ messages: [{
@ -255,12 +282,12 @@ export function Chat({
)} )}
</div> </div>
<div className="flex-1 overflow-auto pr-1 <div
[&::-webkit-scrollbar]{width:4px} ref={scrollContainerRef}
[&::-webkit-scrollbar-track]{background:transparent} onScroll={handleScroll}
[&::-webkit-scrollbar-thumb]{background-color:rgb(156 163 175)} className="flex-1 overflow-auto pr-4 relative playground-scrollbar"
dark:[&::-webkit-scrollbar-thumb]{background-color:#2a2d31}"> style={{ scrollBehavior: 'smooth' }}
<div className="pr-4"> >
<Messages <Messages
projectId={projectId} projectId={projectId}
messages={optimisticMessages} messages={optimisticMessages}
@ -273,7 +300,22 @@ export function Chat({
showSystemMessage={false} showSystemMessage={false}
showDebugMessages={showDebugMessages} showDebugMessages={showDebugMessages}
/> />
</div> {showUnreadBubble && (
<button
className="absolute bottom-4 right-4 z-20 bg-blue-100 text-blue-700 rounded-full w-8 h-8 flex items-center justify-center hover:bg-blue-200 transition-colors animate-pulse"
onClick={() => {
const container = scrollContainerRef.current;
if (container) {
container.scrollTop = container.scrollHeight;
}
setAutoScroll(true);
setShowUnreadBubble(false);
}}
aria-label="Scroll to latest message"
>
<ChevronDownIcon className="w-5 h-5" strokeWidth={2.2} />
</button>
)}
</div> </div>
<div className="sticky bottom-0 bg-white dark:bg-zinc-900 pt-4 pb-2"> <div className="sticky bottom-0 bg-white dark:bg-zinc-900 pt-4 pb-2">

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { Spinner } from "@heroui/react"; import { Spinner } from "@heroui/react";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState, useCallback } from "react";
import z from "zod"; import z from "zod";
import { Workflow } from "@/app/lib/types/workflow_types"; import { Workflow } from "@/app/lib/types/workflow_types";
import { WorkflowTool } from "@/app/lib/types/workflow_types"; import { WorkflowTool } from "@/app/lib/types/workflow_types";
@ -360,13 +360,11 @@ export function Messages({
showSystemMessage: boolean; showSystemMessage: boolean;
showDebugMessages?: boolean; showDebugMessages?: boolean;
}) { }) {
const messagesEndRef = useRef<HTMLDivElement>(null); // Remove scroll/auto-scroll state and logic
let lastUserMessageTimestamp = 0; // const scrollContainerRef = useRef<HTMLDivElement>(null);
let userMessageSeen = false; // const [autoScroll, setAutoScroll] = useState(true);
// const [showUnreadBubble, setShowUnreadBubble] = useState(false);
useEffect(() => { // Remove handleScroll and useEffect for scroll
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, loadingAssistantResponse]);
const renderMessage = (message: z.infer<typeof Message>, index: number) => { const renderMessage = (message: z.infer<typeof Message>, index: number) => {
if (message.role === 'assistant') { if (message.role === 'assistant') {
@ -427,7 +425,7 @@ export function Messages({
if (message.role === 'user') { if (message.role === 'user') {
// TODO: add latency support // TODO: add latency support
// lastUserMessageTimestamp = new Date(message.createdAt).getTime(); // lastUserMessageTimestamp = new Date(message.createdAt).getTime();
userMessageSeen = true; // userMessageSeen = true;
return <UserMessage content={message.content} />; return <UserMessage content={message.content} />;
} }
@ -452,9 +450,9 @@ export function Messages({
); );
} }
// Just render the messages, no scroll container or unread bubble
return ( return (
<div className="max-w-[768px] mx-auto"> <div className="max-w-4xl mx-auto px-2 sm:px-6 relative">
<div className="flex flex-col">
{messages.map((message, index) => { {messages.map((message, index) => {
const renderedMessage = renderMessage(message, index); const renderedMessage = renderMessage(message, index);
if (renderedMessage) { if (renderedMessage) {
@ -468,7 +466,5 @@ export function Messages({
})} })}
{loadingAssistantResponse && <AssistantMessageLoading />} {loadingAssistantResponse && <AssistantMessageLoading />}
</div> </div>
<div ref={messagesEndRef} />
</div>
); );
} }