hsaest Cursor commited on
Commit
9677388
·
1 Parent(s): 532504e

feat: output panel internal scrollbar with auto-follow

Browse files
Files changed (1) hide show
  1. app.py +65 -81
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
- /* Prevent wrappers INSIDE the output section from becoming internal scroll
594
- containers. If they clip content the auto-follow scrolls them instead of the
595
- document, making it impossible to scroll back to the top of the page.
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
- #quest-output .tab-content,
601
- #quest-output .tabs > div:not(.tab-nav),
602
- #quest-output .block,
603
- #quest-output .wrap,
604
- #quest-output .contain {
 
 
 
 
 
 
 
 
 
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
- // Walk up from an element to find the nearest vertically-scrollable ancestor.
3156
- // Gradio/HF may scroll a wrapper div instead of the window.
3157
- function scrollableAncestor(el) {
3158
- var node = el.parentElement;
3159
- while (node && node !== document.body && node !== document.documentElement) {
3160
- var oy = getComputedStyle(node).overflowY;
3161
- if ((oy === 'auto' || oy === 'scroll') && node.scrollHeight > node.clientHeight + 2) {
3162
- return node;
3163
- }
3164
- node = node.parentElement;
3165
  }
3166
- return document.scrollingElement || document.documentElement;
3167
  }
3168
 
3169
- // Scroll so the output is in view.
3170
- // - alignEnd=false (click): use scrollIntoView for reliability across layouts.
3171
- // - alignEnd=true (auto-follow): use explicit window.scrollTo so we only
3172
- // move the document and never accidentally scroll an inner wrapper, which
3173
- // would make it impossible to scroll back to the top afterwards.
3174
- function scrollToOutput(alignEnd) {
 
 
 
 
 
3175
  var out = document.getElementById('quest-output');
3176
  if (!out) return;
3177
- ignoreScroll = true;
3178
- if (!alignEnd) {
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 land through layout-shift during the first second.
3202
- function scrollToOutputRetry(alignEnd) {
3203
- scrollToOutput(alignEnd);
3204
- [150, 400, 800, 1400].forEach(function (ms) {
3205
- setTimeout(function () { scrollToOutput(alignEnd); }, ms);
3206
  });
3207
  }
3208
 
3209
- // Update autoFollow based on whether the user is near the bottom of the
3210
- // real scroll container (not just the document root).
3211
- function onScrollEvent() {
3212
  if (ignoreScroll) return;
3213
- var out = document.getElementById('quest-output');
3214
- var container = out
3215
- ? scrollableAncestor(out)
3216
- : (document.scrollingElement || document.documentElement);
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
- // If the real scroll container is an inner element, also listen there so
3234
- // that user scrolling within it correctly flips autoFollow off.
3235
- var container = scrollableAncestor(out);
3236
- var isDocRoot = (container === document.documentElement ||
3237
- container === (document.scrollingElement || document.documentElement));
3238
- if (!isDocRoot) {
3239
- container.addEventListener('scroll', onScrollEvent, { passive: true });
3240
  }
 
3241
 
3242
  new MutationObserver(function () {
3243
- if (autoFollow) scrollToOutput(true);
3244
  }).observe(out, { childList: true, subtree: true });
3245
  }
3246
  watchOutput();
3247
 
3248
- // On Run click: jump to top of output and re-enable following.
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(false);
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
  }