Skip to content

Commit 2bb71f4

Browse files
feat(web): add scroll to bottom pill in chat view (pingdotgg#619)
1 parent cc2ab00 commit 2bb71f4

File tree

1 file changed

+59
-40
lines changed

1 file changed

+59
-40
lines changed

apps/web/src/components/ChatView.tsx

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
242242
(store) => store.draftThreadsByThreadId[threadId] ?? null,
243243
);
244244
const promptRef = useRef(prompt);
245+
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
245246
const [isDragOverComposer, setIsDragOverComposer] = useState(false);
246247
const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null);
247248
const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]);
@@ -1587,6 +1588,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
15871588
}
15881589
}
15891590

1591+
setShowScrollToBottom(!shouldAutoScrollRef.current);
15901592
lastKnownScrollTopRef.current = currentScrollTop;
15911593
}, []);
15921594
const onMessagesWheel = useCallback((event: React.WheelEvent<HTMLDivElement>) => {
@@ -3259,45 +3261,62 @@ export default function ChatView({ threadId }: ChatViewProps) {
32593261
<div className="flex min-h-0 min-w-0 flex-1">
32603262
{/* Chat column */}
32613263
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
3262-
{/* Messages */}
3263-
<div
3264-
ref={setMessagesScrollContainerRef}
3265-
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-y-contain px-3 py-3 sm:px-5 sm:py-4"
3266-
onScroll={onMessagesScroll}
3267-
onClickCapture={onMessagesClickCapture}
3268-
onWheel={onMessagesWheel}
3269-
onPointerDown={onMessagesPointerDown}
3270-
onPointerUp={onMessagesPointerUp}
3271-
onPointerCancel={onMessagesPointerCancel}
3272-
onTouchStart={onMessagesTouchStart}
3273-
onTouchMove={onMessagesTouchMove}
3274-
onTouchEnd={onMessagesTouchEnd}
3275-
onTouchCancel={onMessagesTouchEnd}
3276-
>
3277-
<MessagesTimeline
3278-
key={activeThread.id}
3279-
hasMessages={timelineEntries.length > 0}
3280-
isWorking={isWorking}
3281-
activeTurnInProgress={isWorking || !latestTurnSettled}
3282-
activeTurnStartedAt={activeWorkStartedAt}
3283-
scrollContainer={messagesScrollElement}
3284-
timelineEntries={timelineEntries}
3285-
completionDividerBeforeEntryId={completionDividerBeforeEntryId}
3286-
completionSummary={completionSummary}
3287-
turnDiffSummaryByAssistantMessageId={turnDiffSummaryByAssistantMessageId}
3288-
nowIso={nowIso}
3289-
expandedWorkGroups={expandedWorkGroups}
3290-
onToggleWorkGroup={onToggleWorkGroup}
3291-
onOpenTurnDiff={onOpenTurnDiff}
3292-
revertTurnCountByUserMessageId={revertTurnCountByUserMessageId}
3293-
onRevertUserMessage={onRevertUserMessage}
3294-
isRevertingCheckpoint={isRevertingCheckpoint}
3295-
onImageExpand={onExpandTimelineImage}
3296-
markdownCwd={gitCwd ?? undefined}
3297-
resolvedTheme={resolvedTheme}
3298-
timestampFormat={timestampFormat}
3299-
workspaceRoot={activeProject?.cwd ?? undefined}
3300-
/>
3264+
{/* Messages Wrapper */}
3265+
<div className="relative flex min-h-0 flex-1 flex-col">
3266+
{/* Messages */}
3267+
<div
3268+
ref={setMessagesScrollContainerRef}
3269+
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-y-contain px-3 py-3 sm:px-5 sm:py-4"
3270+
onScroll={onMessagesScroll}
3271+
onClickCapture={onMessagesClickCapture}
3272+
onWheel={onMessagesWheel}
3273+
onPointerDown={onMessagesPointerDown}
3274+
onPointerUp={onMessagesPointerUp}
3275+
onPointerCancel={onMessagesPointerCancel}
3276+
onTouchStart={onMessagesTouchStart}
3277+
onTouchMove={onMessagesTouchMove}
3278+
onTouchEnd={onMessagesTouchEnd}
3279+
onTouchCancel={onMessagesTouchEnd}
3280+
>
3281+
<MessagesTimeline
3282+
key={activeThread.id}
3283+
hasMessages={timelineEntries.length > 0}
3284+
isWorking={isWorking}
3285+
activeTurnInProgress={isWorking || !latestTurnSettled}
3286+
activeTurnStartedAt={activeWorkStartedAt}
3287+
scrollContainer={messagesScrollElement}
3288+
timelineEntries={timelineEntries}
3289+
completionDividerBeforeEntryId={completionDividerBeforeEntryId}
3290+
completionSummary={completionSummary}
3291+
turnDiffSummaryByAssistantMessageId={turnDiffSummaryByAssistantMessageId}
3292+
nowIso={nowIso}
3293+
expandedWorkGroups={expandedWorkGroups}
3294+
onToggleWorkGroup={onToggleWorkGroup}
3295+
onOpenTurnDiff={onOpenTurnDiff}
3296+
revertTurnCountByUserMessageId={revertTurnCountByUserMessageId}
3297+
onRevertUserMessage={onRevertUserMessage}
3298+
isRevertingCheckpoint={isRevertingCheckpoint}
3299+
onImageExpand={onExpandTimelineImage}
3300+
markdownCwd={gitCwd ?? undefined}
3301+
resolvedTheme={resolvedTheme}
3302+
timestampFormat={timestampFormat}
3303+
workspaceRoot={activeProject?.cwd ?? undefined}
3304+
/>
3305+
</div>
3306+
3307+
{/* scroll to bottom pill — shown when user has scrolled away from the bottom */}
3308+
{showScrollToBottom && (
3309+
<div className="pointer-events-none absolute bottom-1 left-1/2 z-30 flex -translate-x-1/2 justify-center py-1.5">
3310+
<button
3311+
type="button"
3312+
onClick={() => scrollMessagesToBottom("smooth")}
3313+
className="pointer-events-auto flex items-center gap-1.5 rounded-full border border-border/60 bg-card px-3 py-1 text-muted-foreground text-xs shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
3314+
>
3315+
<ChevronDownIcon className="size-3.5" />
3316+
Scroll to bottom
3317+
</button>
3318+
</div>
3319+
)}
33013320
</div>
33023321

33033322
{/* Input bar */}
@@ -3328,7 +3347,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
33283347
<div className="rounded-t-[19px] border-b border-border/65 bg-muted/20">
33293348
<ComposerPendingUserInputPanel
33303349
pendingUserInputs={pendingUserInputs}
3331-
respondingRequestIds={respondingUserInputRequestIds}
3350+
respondingRequestIds={respondingRequestIds}
33323351
answers={activePendingDraftAnswers}
33333352
questionIndex={activePendingQuestionIndex}
33343353
onSelectOption={onSelectActivePendingUserInputOption}

0 commit comments

Comments
 (0)