11"use client" ;
22
33import { useParams , useSearchParams , useRouter } from "next/navigation" ;
4- import { useMemo , useState } from "react" ;
4+ import { useMemo , useRef , useState } from "react" ;
55import { useRealtimeRun , useRealtimeStream } from "@trigger.dev/react-hooks" ;
66import { Streamdown } from "streamdown" ;
77import { ArrowLeft , AlertCircle , Calendar , Copy , Check } from "lucide-react" ;
@@ -88,6 +88,8 @@ export default function ResponsePage() {
8888 const searchParams = useSearchParams ( ) ;
8989 const router = useRouter ( ) ;
9090 const [ copiedIndex , setCopiedIndex ] = useState < number | null > ( null ) ;
91+ const userScrolledRef = useRef ( false ) ;
92+ const lastScrollTop = useRef ( 0 ) ;
9193
9294 const runId = params . runId as string ;
9395 const accessToken = searchParams . get ( "accessToken" ) || "" ;
@@ -110,6 +112,21 @@ export default function ResponsePage() {
110112 router . push ( "/" ) ;
111113 } ;
112114
115+ // Detect if user scrolled up (away from bottom) to cancel auto-scroll
116+ const handleScroll = ( ) => {
117+ const scrollTop = window . scrollY ;
118+ // User scrolled up - cancel auto-scroll
119+ if ( scrollTop < lastScrollTop . current - 50 ) {
120+ userScrolledRef . current = true ;
121+ }
122+ // User scrolled to bottom - re-enable auto-scroll
123+ const isNearBottom = window . innerHeight + scrollTop >= document . body . scrollHeight - 100 ;
124+ if ( isNearBottom ) {
125+ userScrolledRef . current = false ;
126+ }
127+ lastScrollTop . current = scrollTop ;
128+ } ;
129+
113130 const handleCopySection = async (
114131 section : { title : string ; content : string ; category ?: string } ,
115132 index : number
@@ -151,7 +168,7 @@ export default function ResponsePage() {
151168 const metadata = run ?. metadata as RunMetadata | undefined ;
152169
153170 return (
154- < div className = "min-h-screen bg-background" >
171+ < div className = "min-h-screen bg-background" onScroll = { handleScroll } onWheel = { handleScroll } >
155172 < div className = "container mx-auto px-4 py-12 max-w-2xl" >
156173 { /* Header */ }
157174 < header className = "mb-8" >
@@ -248,24 +265,26 @@ export default function ResponsePage() {
248265 < Card key = { i } >
249266 < CardHeader className = "flex gap-4" >
250267 < div className = "flex items-center w-full justify-between gap-4 shrink-0" >
251- < div className = "gap-3 flex items-center" >
252- { section . category && (
253- < span className = "text-xs font-medium text-muted-foreground bg-secondary px-2 py-0.5 rounded" >
254- { section . category }
255- </ span >
256- ) }
257- { section . date && (
258- < span className = "text-xs text-muted-foreground" >
259- { section . date }
260- </ span >
261- ) }
262- </ div >
268+ { ( section . category || section . date ) && (
269+ < div className = "gap-3 flex items-center" >
270+ { section . category && (
271+ < span className = "text-xs font-medium text-muted-foreground bg-secondary px-2 py-0.5 rounded" >
272+ { section . category }
273+ </ span >
274+ ) }
275+ { section . date && (
276+ < span className = "text-xs text-muted-foreground" >
277+ { section . date }
278+ </ span >
279+ ) }
280+ </ div >
281+ ) }
263282
264283 < Button
265284 variant = "ghost"
266285 size = "sm"
267286 onClick = { ( ) => handleCopySection ( section , i ) }
268- className = "text-xs text-muted-foreground hover:text-foreground"
287+ className = "ml-auto text-xs text-muted-foreground hover:text-foreground"
269288 >
270289 { copiedIndex === i ? (
271290 < >
@@ -287,6 +306,7 @@ export default function ResponsePage() {
287306 isAnimating = { isStreaming && i === sections . length - 1 }
288307 mode = "streaming"
289308 shikiTheme = { [ "github-dark" , "github-dark" ] }
309+ className = "text-foreground/85"
290310 >
291311 { section . content }
292312 </ Streamdown >
@@ -305,6 +325,15 @@ export default function ResponsePage() {
305325 </ div >
306326 ) }
307327
328+ { /* Scroll anchor - auto-scrolls unless user scrolled away */ }
329+ < div
330+ ref = { ( el ) => {
331+ if ( el && isStreaming && ! userScrolledRef . current ) {
332+ el . scrollIntoView ( { behavior : "smooth" , block : "end" } ) ;
333+ }
334+ } }
335+ />
336+
308337 { /* Completion Actions */ }
309338 { status === "completed" && (
310339 < div className = "mt-8" >
0 commit comments