Spaces:
Running
Running
feat: output panel internal scrollbar with auto-follow
Browse files
app.py
CHANGED
|
@@ -590,18 +590,25 @@ gradio-app > div {
|
|
| 590 |
[class*="gradio-container"] .codemirror-wrapper,
|
| 591 |
[class*="gradio-container"] .cm-editor { min-height: 220px !important; }
|
| 592 |
|
| 593 |
-
/*
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
We scope this narrowly to #quest-output so it does not disturb buttons or
|
| 597 |
-
other parts of the UI. */
|
| 598 |
#quest-output .tabitem,
|
| 599 |
-
#quest-output [role="tabpanel"]
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 605 |
overflow: visible !important;
|
| 606 |
max-height: none !important;
|
| 607 |
height: auto !important;
|
|
@@ -3149,108 +3156,85 @@ function() {
|
|
| 3149 |
|
| 3150 |
|
| 3151 |
// === Auto-scroll ===
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3152 |
var autoFollow = true;
|
| 3153 |
var ignoreScroll = false;
|
| 3154 |
|
| 3155 |
-
//
|
| 3156 |
-
|
| 3157 |
-
|
| 3158 |
-
|
| 3159 |
-
|
| 3160 |
-
|
| 3161 |
-
|
| 3162 |
-
|
| 3163 |
-
|
| 3164 |
-
node = node.parentElement;
|
| 3165 |
}
|
| 3166 |
-
return
|
| 3167 |
}
|
| 3168 |
|
| 3169 |
-
// Scroll
|
| 3170 |
-
|
| 3171 |
-
|
| 3172 |
-
|
| 3173 |
-
|
| 3174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3175 |
var out = document.getElementById('quest-output');
|
| 3176 |
if (!out) return;
|
| 3177 |
-
|
| 3178 |
-
|
| 3179 |
-
// Jump to top of output on button click — scrollIntoView is the most
|
| 3180 |
-
// reliable cross-browser way to handle this.
|
| 3181 |
-
try { out.scrollIntoView({ block: 'start' }); } catch (_) {}
|
| 3182 |
-
} else {
|
| 3183 |
-
// Auto-follow: only scroll the document/viewport, never inner containers.
|
| 3184 |
-
var container = scrollableAncestor(out);
|
| 3185 |
-
var isDocRoot = (container === document.documentElement ||
|
| 3186 |
-
container === (document.scrollingElement || document.documentElement));
|
| 3187 |
-
var cTop = isDocRoot ? 0 : container.getBoundingClientRect().top;
|
| 3188 |
-
var oRect = out.getBoundingClientRect();
|
| 3189 |
-
var viewH = isDocRoot ? window.innerHeight : container.clientHeight;
|
| 3190 |
-
var delta = (oRect.bottom - cTop) - viewH + 24;
|
| 3191 |
-
var newTop = Math.max(0, container.scrollTop + delta);
|
| 3192 |
-
if (isDocRoot) {
|
| 3193 |
-
window.scrollTo(0, newTop);
|
| 3194 |
-
} else {
|
| 3195 |
-
container.scrollTop = newTop;
|
| 3196 |
-
}
|
| 3197 |
}
|
| 3198 |
-
setTimeout(function () { ignoreScroll = false; }, 700);
|
| 3199 |
}
|
| 3200 |
|
| 3201 |
-
// Retry a few times to
|
| 3202 |
-
function scrollToOutputRetry(
|
| 3203 |
-
|
| 3204 |
-
[
|
| 3205 |
-
setTimeout(
|
| 3206 |
});
|
| 3207 |
}
|
| 3208 |
|
| 3209 |
-
//
|
| 3210 |
-
|
| 3211 |
-
function onScrollEvent() {
|
| 3212 |
if (ignoreScroll) return;
|
| 3213 |
-
var
|
| 3214 |
-
|
| 3215 |
-
|
| 3216 |
-
|
| 3217 |
-
var isDocRoot = (container === document.documentElement ||
|
| 3218 |
-
container === (document.scrollingElement || document.documentElement));
|
| 3219 |
-
var scrollTop = isDocRoot
|
| 3220 |
-
? (window.scrollY || document.documentElement.scrollTop || 0)
|
| 3221 |
-
: container.scrollTop;
|
| 3222 |
-
var viewH = isDocRoot ? window.innerHeight : container.clientHeight;
|
| 3223 |
-
autoFollow = (container.scrollHeight - scrollTop - viewH) < 320;
|
| 3224 |
}
|
| 3225 |
|
| 3226 |
-
// Listen on the window (document-level scroll) always.
|
| 3227 |
-
window.addEventListener('scroll', onScrollEvent, { passive: true });
|
| 3228 |
-
|
| 3229 |
function watchOutput() {
|
| 3230 |
var out = document.getElementById('quest-output');
|
| 3231 |
if (!out) { setTimeout(watchOutput, 600); return; }
|
| 3232 |
|
| 3233 |
-
//
|
| 3234 |
-
|
| 3235 |
-
|
| 3236 |
-
|
| 3237 |
-
|
| 3238 |
-
if (!isDocRoot) {
|
| 3239 |
-
container.addEventListener('scroll', onScrollEvent, { passive: true });
|
| 3240 |
}
|
|
|
|
| 3241 |
|
| 3242 |
new MutationObserver(function () {
|
| 3243 |
-
if (autoFollow)
|
| 3244 |
}).observe(out, { childList: true, subtree: true });
|
| 3245 |
}
|
| 3246 |
watchOutput();
|
| 3247 |
|
| 3248 |
-
// On Run click:
|
| 3249 |
document.addEventListener('click', function (e) {
|
| 3250 |
var wrapper = document.getElementById('quest-run-btn');
|
| 3251 |
if (wrapper && wrapper.contains(e.target)) {
|
| 3252 |
autoFollow = true;
|
| 3253 |
-
scrollToOutputRetry(
|
| 3254 |
}
|
| 3255 |
}, true);
|
| 3256 |
}
|
|
|
|
| 590 |
[class*="gradio-container"] .codemirror-wrapper,
|
| 591 |
[class*="gradio-container"] .cm-editor { min-height: 220px !important; }
|
| 592 |
|
| 593 |
+
/* Output tab panels: fixed viewport-relative height with a vertical scrollbar.
|
| 594 |
+
This keeps the page layout stable while the research streams in, and lets
|
| 595 |
+
the user scroll output content independently of the rest of the page. */
|
|
|
|
|
|
|
| 596 |
#quest-output .tabitem,
|
| 597 |
+
#quest-output [role="tabpanel"] {
|
| 598 |
+
overflow-y: auto !important;
|
| 599 |
+
min-height: 320px !important;
|
| 600 |
+
max-height: 62vh !important;
|
| 601 |
+
height: auto !important;
|
| 602 |
+
overscroll-behavior: contain;
|
| 603 |
+
}
|
| 604 |
+
/* Inner blocks / wraps inside the output must NOT clip or fix their own height
|
| 605 |
+
so the content can grow freely within the tab panel scroller. */
|
| 606 |
+
#quest-output .tabitem .block,
|
| 607 |
+
#quest-output .tabitem .wrap,
|
| 608 |
+
#quest-output .tabitem .contain,
|
| 609 |
+
#quest-output [role="tabpanel"] .block,
|
| 610 |
+
#quest-output [role="tabpanel"] .wrap,
|
| 611 |
+
#quest-output [role="tabpanel"] .contain {
|
| 612 |
overflow: visible !important;
|
| 613 |
max-height: none !important;
|
| 614 |
height: auto !important;
|
|
|
|
| 3156 |
|
| 3157 |
|
| 3158 |
// === Auto-scroll ===
|
| 3159 |
+
// Design: the Output tab panel (#quest-output .tabitem) is the intentional
|
| 3160 |
+
// scroll container (fixed max-height + overflow-y:auto via CSS).
|
| 3161 |
+
// auto-follow scrolls that inner panel to the bottom as content streams in.
|
| 3162 |
+
// Clicking Run Research scrolls the PAGE to show the output card.
|
| 3163 |
var autoFollow = true;
|
| 3164 |
var ignoreScroll = false;
|
| 3165 |
|
| 3166 |
+
// Return the active, visible tab panel inside #quest-output.
|
| 3167 |
+
function outputPanel() {
|
| 3168 |
+
var out = document.getElementById('quest-output');
|
| 3169 |
+
if (!out) return null;
|
| 3170 |
+
// Prefer the visible tabitem with actual scroll room.
|
| 3171 |
+
var panels = out.querySelectorAll('.tabitem, [role="tabpanel"]');
|
| 3172 |
+
for (var i = 0; i < panels.length; i++) {
|
| 3173 |
+
var p = panels[i];
|
| 3174 |
+
if (p.offsetParent !== null) return p; // visible panel
|
|
|
|
| 3175 |
}
|
| 3176 |
+
return panels[0] || null;
|
| 3177 |
}
|
| 3178 |
|
| 3179 |
+
// Scroll the inner panel to the bottom (auto-follow during streaming).
|
| 3180 |
+
function followOutputBottom() {
|
| 3181 |
+
var panel = outputPanel();
|
| 3182 |
+
if (!panel) return;
|
| 3183 |
+
ignoreScroll = true;
|
| 3184 |
+
panel.scrollTop = panel.scrollHeight;
|
| 3185 |
+
setTimeout(function () { ignoreScroll = false; }, 300);
|
| 3186 |
+
}
|
| 3187 |
+
|
| 3188 |
+
// Scroll the PAGE so the top of #quest-output is visible (on Run click).
|
| 3189 |
+
function scrollPageToOutput() {
|
| 3190 |
var out = document.getElementById('quest-output');
|
| 3191 |
if (!out) return;
|
| 3192 |
+
try { out.scrollIntoView({ block: 'start', behavior: 'smooth' }); } catch (_) {
|
| 3193 |
+
out.scrollIntoView(true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3194 |
}
|
|
|
|
| 3195 |
}
|
| 3196 |
|
| 3197 |
+
// Retry scrollPageToOutput a few times to handle layout shifts.
|
| 3198 |
+
function scrollToOutputRetry() {
|
| 3199 |
+
scrollPageToOutput();
|
| 3200 |
+
[200, 500, 1000].forEach(function (ms) {
|
| 3201 |
+
setTimeout(scrollPageToOutput, ms);
|
| 3202 |
});
|
| 3203 |
}
|
| 3204 |
|
| 3205 |
+
// When user scrolls the inner panel, decide whether to keep auto-following.
|
| 3206 |
+
function onPanelScroll() {
|
|
|
|
| 3207 |
if (ignoreScroll) return;
|
| 3208 |
+
var panel = outputPanel();
|
| 3209 |
+
if (!panel) return;
|
| 3210 |
+
var distFromBottom = panel.scrollHeight - panel.scrollTop - panel.clientHeight;
|
| 3211 |
+
autoFollow = distFromBottom < 80;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3212 |
}
|
| 3213 |
|
|
|
|
|
|
|
|
|
|
| 3214 |
function watchOutput() {
|
| 3215 |
var out = document.getElementById('quest-output');
|
| 3216 |
if (!out) { setTimeout(watchOutput, 600); return; }
|
| 3217 |
|
| 3218 |
+
// Attach scroll listener to the panel once it exists.
|
| 3219 |
+
function attachPanelListener() {
|
| 3220 |
+
var panel = outputPanel();
|
| 3221 |
+
if (!panel) { setTimeout(attachPanelListener, 400); return; }
|
| 3222 |
+
panel.addEventListener('scroll', onPanelScroll, { passive: true });
|
|
|
|
|
|
|
| 3223 |
}
|
| 3224 |
+
attachPanelListener();
|
| 3225 |
|
| 3226 |
new MutationObserver(function () {
|
| 3227 |
+
if (autoFollow) followOutputBottom();
|
| 3228 |
}).observe(out, { childList: true, subtree: true });
|
| 3229 |
}
|
| 3230 |
watchOutput();
|
| 3231 |
|
| 3232 |
+
// On Run click: scroll page to show output card and re-enable auto-follow.
|
| 3233 |
document.addEventListener('click', function (e) {
|
| 3234 |
var wrapper = document.getElementById('quest-run-btn');
|
| 3235 |
if (wrapper && wrapper.contains(e.target)) {
|
| 3236 |
autoFollow = true;
|
| 3237 |
+
scrollToOutputRetry();
|
| 3238 |
}
|
| 3239 |
}, true);
|
| 3240 |
}
|