mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-21 17:44:11 +00:00
🩹 fix: PPTX slides scale to fit panel width (no horizontal scroll)
Manual e2e on PR #12934: pptx-preview rendered slides at their native init dimensions (960×540 default). The artifact panel is much narrower than that, so the iframe got a horizontal scrollbar and only a corner of each slide showed at any time — the user had to drag-scroll across each slide to read it. Fix: keep pptx-preview's init at 960×540 so its internal layout math stays correct, then post-process each rendered slide: - Cache the slide's native width/height on its dataset BEFORE applying any transform (so subsequent re-fits don't measure the already-transformed box). - Wrap the slide in `.lc-slide-wrap` with explicit width/height set inline to the scaled dimensions; the wrap shrinks the layout space the slide occupies. - Apply `transform: scale(panel_width / 960)` to the slide itself with `transform-origin: top left` so the rendered output shrinks from the top-left corner into the wrap. - Cap the scale at 1.0 so small slides don't upscale and get blurry. Streaming + resize: - `MutationObserver` watches the container for slide insertions so streaming renders get scaled on arrival rather than waiting for the entire `previewer.preview` promise to settle. - `ResizeObserver` re-fits all wrapped slides when the iframe resizes (panel drag, window resize). Tests: new "bootstrap wraps + scales each slide" lock in the wrap class, scale computation, observer setup, and native-size caching so a future refactor can't silently re-introduce the overflow.
This commit is contained in:
parent
832b8f7bc7
commit
02fa6da8d5
2 changed files with 136 additions and 4 deletions
|
|
@ -459,6 +459,32 @@ describe('Office HTML producers', () => {
|
|||
test('size-fallback threshold is the documented 350 KB', () => {
|
||||
expect(_internal.MAX_PPTX_CDN_BINARY_BYTES).toBe(350 * 1024);
|
||||
});
|
||||
|
||||
test('bootstrap wraps + scales each slide so it fits the iframe width', async () => {
|
||||
/* pptx-preview emits slides at the init dimensions (960×540
|
||||
* by default). Without post-processing, narrow artifact panels
|
||||
* scroll horizontally and slides spill outside the viewport.
|
||||
* The bootstrap wraps each rendered slide in `.lc-slide-wrap`
|
||||
* and applies `transform: scale(panel_width / 960)` so the
|
||||
* panel always fits — manual e2e feedback on PR #12934. */
|
||||
const pptx = await buildPptx([{ title: 'A' }, { title: 'B' }]);
|
||||
const html = await _internal.pptxToHtmlViaCdn(pptx);
|
||||
/* The wrapper class — used by the CSS rules that own the
|
||||
* sized container. */
|
||||
expect(html).toContain('lc-slide-wrap');
|
||||
/* The bootstrap script's wrap function name + the scale
|
||||
* computation derived from `clientWidth / SLIDE_W`. */
|
||||
expect(html).toContain('wrapAndFit');
|
||||
expect(html).toContain('SLIDE_W');
|
||||
expect(html).toContain('available / SLIDE_W');
|
||||
/* MutationObserver watches for streaming slide insertions;
|
||||
* ResizeObserver re-fits on panel resize. */
|
||||
expect(html).toContain('MutationObserver');
|
||||
expect(html).toContain('ResizeObserver');
|
||||
/* Native dimensions cached on the slide so re-fitting on
|
||||
* resize doesn't measure the already-transformed box. */
|
||||
expect(html).toContain('lcNativeW');
|
||||
});
|
||||
});
|
||||
|
||||
describe('OFFICE_PREVIEW_DISABLE_CDN escape hatch', () => {
|
||||
|
|
|
|||
|
|
@ -1073,8 +1073,29 @@ function buildPptxCdnDocument(base64: string): string {
|
|||
:root { color-scheme: light dark; --bg: #ffffff; --fg: #1f2937; --muted: #6b7280; }
|
||||
@media (prefers-color-scheme: dark) { :root { --bg: #1a1a2e; --fg: #e5e7eb; --muted: #9ca3af; } }
|
||||
html, body { margin: 0; padding: 0; background: var(--bg); color: var(--fg); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
|
||||
#lc-render { padding: 16px; display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
||||
#lc-render > * { box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); border-radius: 4px; max-width: 100%; }
|
||||
#lc-render { padding: 16px; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; gap: 16px; }
|
||||
/* Each rendered slide is wrapped post-hoc by the bootstrap script in
|
||||
* an .lc-slide-wrap div with explicit width/height set inline. The
|
||||
* inner slide keeps its native pixel size and gets transformed via
|
||||
* a CSS scale so it fits the iframe width without horizontal
|
||||
* scrolling. Without this, pptx-preview emits slides at whatever
|
||||
* resolution it computed against (typically 960×540) and overflows
|
||||
* narrow panels — manual e2e feedback on PR #12934. */
|
||||
.lc-slide-wrap {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.lc-slide-wrap > * {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform-origin: top left;
|
||||
}
|
||||
#lc-fallback { padding: 24px; font-size: 14px; line-height: 1.5; color: var(--muted); text-align: center; }
|
||||
.lc-pptx-loading { display: flex; align-items: center; justify-content: center; height: 60vh; color: var(--muted); font-size: 14px; }
|
||||
</style>
|
||||
|
|
@ -1129,16 +1150,101 @@ html, body { margin: 0; padding: 0; background: var(--bg); color: var(--fg); fon
|
|||
var container = document.getElementById('lc-render');
|
||||
var loading = document.querySelector('.lc-pptx-loading');
|
||||
if (loading) { loading.remove(); }
|
||||
var previewer = pptxPreview.init(container, { width: 960, height: 540 });
|
||||
/* pptx-preview renders into a fixed pixel size; we keep it at the
|
||||
* native 16:9 default so its internal layout calculations are
|
||||
* right, then post-process to scale each slide to the iframe's
|
||||
* width via CSS transform. The wrap+transform approach (vs.
|
||||
* shrinking the init dimensions) preserves font metrics, image
|
||||
* resolution, and chart positioning that pptx-preview computed
|
||||
* against the 960×540 canvas. */
|
||||
var SLIDE_W = 960, SLIDE_H = 540;
|
||||
var previewer = pptxPreview.init(container, { width: SLIDE_W, height: SLIDE_H });
|
||||
|
||||
/* Wrap a single slide element in a sized container and apply the
|
||||
* scale transform. Native size is cached on the slide's dataset
|
||||
* so subsequent fits don't measure the already-transformed box. */
|
||||
function wrapAndFit(slide, scale) {
|
||||
if (slide.parentNode && slide.parentNode.classList && slide.parentNode.classList.contains('lc-slide-wrap')) {
|
||||
return slide.parentNode;
|
||||
}
|
||||
var nativeW = parseFloat(slide.dataset.lcNativeW);
|
||||
var nativeH = parseFloat(slide.dataset.lcNativeH);
|
||||
if (!nativeW || !nativeH) {
|
||||
nativeW = slide.offsetWidth || SLIDE_W;
|
||||
nativeH = slide.offsetHeight || SLIDE_H;
|
||||
slide.dataset.lcNativeW = String(nativeW);
|
||||
slide.dataset.lcNativeH = String(nativeH);
|
||||
}
|
||||
var wrap = document.createElement('div');
|
||||
wrap.className = 'lc-slide-wrap';
|
||||
slide.parentNode.insertBefore(wrap, slide);
|
||||
wrap.appendChild(slide);
|
||||
wrap.style.width = (nativeW * scale) + 'px';
|
||||
wrap.style.height = (nativeH * scale) + 'px';
|
||||
slide.style.transform = 'scale(' + scale + ')';
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function computeScale() {
|
||||
/* clientWidth excludes vertical scrollbar but includes our
|
||||
* 16px horizontal padding (set via box-sizing:border-box). The
|
||||
* cap at 1.0 means slides smaller than the panel render at 1:1
|
||||
* rather than upscaling and getting blurry. */
|
||||
var available = (container.clientWidth || window.innerWidth) - 32;
|
||||
if (available <= 0) { available = 600; }
|
||||
return Math.min(1, available / SLIDE_W);
|
||||
}
|
||||
|
||||
function fitAll() {
|
||||
var scale = computeScale();
|
||||
/* Re-fit any already-wrapped slides (window resize). */
|
||||
var wraps = container.querySelectorAll('.lc-slide-wrap');
|
||||
for (var i = 0; i < wraps.length; i++) {
|
||||
var wrap = wraps[i];
|
||||
var inner = wrap.firstElementChild;
|
||||
if (!inner) { continue; }
|
||||
var nativeW = parseFloat(inner.dataset.lcNativeW) || SLIDE_W;
|
||||
var nativeH = parseFloat(inner.dataset.lcNativeH) || SLIDE_H;
|
||||
wrap.style.width = (nativeW * scale) + 'px';
|
||||
wrap.style.height = (nativeH * scale) + 'px';
|
||||
inner.style.transform = 'scale(' + scale + ')';
|
||||
}
|
||||
/* Wrap any newly-appended slides (pptx-preview emits them
|
||||
* incrementally as parsing completes). */
|
||||
var children = container.children;
|
||||
for (var j = 0; j < children.length; j++) {
|
||||
var el = children[j];
|
||||
if (el.classList && (el.classList.contains('lc-slide-wrap') || el.classList.contains('lc-pptx-loading'))) {
|
||||
continue;
|
||||
}
|
||||
wrapAndFit(el, scale);
|
||||
}
|
||||
}
|
||||
|
||||
/* Watch the container for slide insertions so streaming renders
|
||||
* get scaled as they arrive (vs. waiting for the whole preview
|
||||
* promise to settle). */
|
||||
if (typeof MutationObserver !== 'undefined') {
|
||||
new MutationObserver(fitAll).observe(container, { childList: true });
|
||||
}
|
||||
|
||||
/* Re-fit on iframe resize (panel drag, window resize). */
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
new ResizeObserver(fitAll).observe(container);
|
||||
} else {
|
||||
window.addEventListener('resize', fitAll);
|
||||
}
|
||||
|
||||
var result = previewer.preview(bytes.buffer);
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(markSuccess).catch(function (err) { showFallback(err && err.message); });
|
||||
result.then(function () { fitAll(); markSuccess(); }).catch(function (err) { showFallback(err && err.message); });
|
||||
}
|
||||
/* Final safety net: if 8 seconds in the renderer hasn't appended
|
||||
* anything visible to the container, assume it failed silently. */
|
||||
setTimeout(function () {
|
||||
if (settled) { return; }
|
||||
if (container && container.children.length > 0) {
|
||||
fitAll();
|
||||
markSuccess();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue