mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-13 17:22:37 +02:00
Prevent forced auto-scroll in playground chat
This commit is contained in:
parent
931e026cdc
commit
3c2bde91b0
3 changed files with 102 additions and 44 deletions
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue