feat: Add pause/auto-scroll toggle to the log viewer
New lines auto-scroll the view to the bottom; scrolling up to read now pauses that (an amber 'paused' marker shows), and scrolling back to the bottom resumes — the explicit Pause/Play button and the scroll position stay in sync via a single at-bottom check. While paused, logs still stream in below without moving the view. Jump-to-latest is instant rather than smooth so mid-animation positions never register as a false scroll-up.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useImperativeHandle, useRef, useState, forwardRef } from "react"
|
import { useEffect, useImperativeHandle, useRef, useState, forwardRef } from "react"
|
||||||
import { Maximize2, Minimize2, Trash2 } from "lucide-react"
|
import { Maximize2, Minimize2, Pause, Play, Trash2 } from "lucide-react"
|
||||||
import { apiClient } from "@/services/api/client"
|
import { apiClient } from "@/services/api/client"
|
||||||
|
|
||||||
interface LogViewerProps {
|
interface LogViewerProps {
|
||||||
@@ -19,7 +19,11 @@ export const LogViewer = forwardRef<LogViewerHandle, LogViewerProps>(function Lo
|
|||||||
const [logs, setLogs] = useState<string[]>([])
|
const [logs, setLogs] = useState<string[]>([])
|
||||||
const [connected, setConnected] = useState(false)
|
const [connected, setConnected] = useState(false)
|
||||||
const [maximized, setMaximized] = useState(false)
|
const [maximized, setMaximized] = useState(false)
|
||||||
|
// When true, new log lines scroll the view to the bottom. Scrolling up to read
|
||||||
|
// pauses it; scrolling back to the bottom (or the play button) resumes.
|
||||||
|
const [autoScroll, setAutoScroll] = useState(true)
|
||||||
const bottomRef = useRef<HTMLDivElement>(null)
|
const bottomRef = useRef<HTMLDivElement>(null)
|
||||||
|
const preRef = useRef<HTMLPreElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!follow) {
|
if (!follow) {
|
||||||
@@ -53,9 +57,20 @@ export const LogViewer = forwardRef<LogViewerHandle, LogViewerProps>(function Lo
|
|||||||
return () => window.removeEventListener("keydown", onKey)
|
return () => window.removeEventListener("keydown", onKey)
|
||||||
}, [maximized])
|
}, [maximized])
|
||||||
|
|
||||||
|
// Jump to the newest line only while auto-scroll is on. Instant (not smooth) so
|
||||||
|
// the scroll-position check below never sees a mid-animation false "scrolled up".
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
bottomRef.current?.scrollIntoView({ behavior: "smooth" })
|
if (autoScroll) bottomRef.current?.scrollIntoView()
|
||||||
}, [logs])
|
}, [logs, autoScroll])
|
||||||
|
|
||||||
|
// Keep auto-scroll in sync with where the user is: away from the bottom pauses,
|
||||||
|
// back at the bottom resumes — so the button and the scroll position never disagree.
|
||||||
|
const onScroll = () => {
|
||||||
|
const el = preRef.current
|
||||||
|
if (!el) return
|
||||||
|
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40
|
||||||
|
setAutoScroll(atBottom)
|
||||||
|
}
|
||||||
|
|
||||||
const btn =
|
const btn =
|
||||||
"p-1 rounded text-[var(--muted)] hover:text-[var(--foreground)] hover:bg-white/10 transition-colors"
|
"p-1 rounded text-[var(--muted)] hover:text-[var(--foreground)] hover:bg-white/10 transition-colors"
|
||||||
@@ -76,8 +91,16 @@ export const LogViewer = forwardRef<LogViewerHandle, LogViewerProps>(function Lo
|
|||||||
{connected ? "streaming" : "disconnected"}
|
{connected ? "streaming" : "disconnected"}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{follow && !autoScroll && <span className="text-amber-400">paused</span>}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-0.5">
|
<span className="flex items-center gap-0.5">
|
||||||
|
<button
|
||||||
|
onClick={() => setAutoScroll((s) => !s)}
|
||||||
|
className={btn}
|
||||||
|
title={autoScroll ? "Pause auto-scroll" : "Resume auto-scroll (jump to latest)"}
|
||||||
|
>
|
||||||
|
{autoScroll ? <Pause size={13} /> : <Play size={13} />}
|
||||||
|
</button>
|
||||||
<button onClick={clear} className={btn} title="Clear">
|
<button onClick={clear} className={btn} title="Clear">
|
||||||
<Trash2 size={13} />
|
<Trash2 size={13} />
|
||||||
</button>
|
</button>
|
||||||
@@ -91,6 +114,8 @@ export const LogViewer = forwardRef<LogViewerHandle, LogViewerProps>(function Lo
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<pre
|
<pre
|
||||||
|
ref={preRef}
|
||||||
|
onScroll={onScroll}
|
||||||
className={`p-3 text-xs font-mono text-gray-300 ${
|
className={`p-3 text-xs font-mono text-gray-300 ${
|
||||||
maximized ? "flex-1 overflow-auto" : "overflow-x-auto max-h-96 overflow-y-auto"
|
maximized ? "flex-1 overflow-auto" : "overflow-x-auto max-h-96 overflow-y-auto"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
Reference in New Issue
Block a user