Skip to content

🗂️ feat: Preview Large PowerPoint and Word Outputs from File Storage - #16518

Open
TomasPalsson wants to merge 32 commits into
LibreChat-AI:devfrom
TomasPalsson:feat/storage-backed-office-previews
Open

TomasPalsson wants to merge 32 commits into
LibreChat-AI:devfrom
TomasPalsson:feat/storage-backed-office-previews

Conversation

@TomasPalsson

@TomasPalsson TomasPalsson commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Pull Request

Built on #16496. This branch starts from the head of #16496 (cb7bfebd0), so until that PR merges the diff below also shows its commits. The changes in this PR are the commits after cb7bfebd0: TomasPalsson/LibreChat@cb7bfeb...b00b09c (the last commit is a merge of dev).

Summary

When code execution produces a PowerPoint or Word file larger than 350 KB, the side panel never shows the real document. The stored preview lives in the file record's text field, which is capped at 512 KB, and the document's own bytes ride inline inside that preview. So files between 350 KB and 2 MB fall back to a plain slide or text outline, and anything over 2 MB is refused before it is opened and labelled parser-error, which shows "Preview unavailable" for a file that downloads fine.

This PR stores a small preview page (a "shell") for .pptx and .docx outputs above 350 KB, with an empty data slot and the existing text fallback. When the panel opens, the browser fetches the file's bytes through the existing authenticated download route (or the share route for a shared link) and fills the slot, so decks and documents up to 25 MB preview as the real document. Files above the limit fail with a new, honest previewError: 'too-large' instead of parser-error. Files of 350 KB or less keep today's inline path unchanged.

The behavior is controlled by a new fileConfig.officePreview setting in librechat.yaml (enabled, default true; fileSizeLimit in MB, default 25, minimum 0).

Related to #16496
Documentation: LibreChat-AI/docs#790

How it works

code output (pptx/docx) ── processCodeOutput / finalizePreview (api, wiring only)
  extractCodeArtifactText(buffer, name, mime, category, officePreview)      packages/api/src/files/code/extract.ts
    ≤ 350 KB                        -> pptxToHtml / wordDocToHtml (inline, as today)
    350 KB < size ≤ byte limit      -> pptxToHtml / wordDocToHtml({ fileShell: true })   packages/api/src/files/documents/html.ts
    preview failed                  -> officePreviewFailure(): 'too-large' if size > byte limit, else 'parser-error'

side panel (SandboxArtifactTabs)
  useOfficeFileShell(artifact)                                              client/src/hooks/Artifacts/useOfficeFileShell.ts
    isOfficeFileShell(content) && download.file_id
      -> useFilePreviewBlob(user, file_id, shareId).refetch()  (download or share route)
      -> fillOfficeFileShell(shell, base64)  -> preview only; the code editor keeps the stored shell
      -> on fetch failure: stored shell unchanged, its fallback shows

The contract (OFFICE_FILE_SHELL_MARKER, OFFICE_DOC_DATA_SLOT, isOfficeFileShell, fillOfficeFileShell) and the setting live in packages/data-provider/src/file-config.ts, so server and client share one definition. The byte limit is enabled ? max(2 MB, fileSizeLimit) : 2 MB for pptx/docx and 2 MB for everything else. LibreOffice, when enabled, still runs first and is untouched. The shell is never base64-encoded on the server, so a 25 MB deck is not copied into a ~33 MB string.

Mixed versions: an older client that receives a shell sees an empty data slot, and both bootstraps now show the text fallback immediately (no-data) instead of waiting for the renderer timeout. A newer client reading an older inline row does not fetch anything and renders it as before. If a shell's fallback would push it past 512 KB, the fallback is dropped and the notice says the document is too large for the simplified preview and can be downloaded, rather than pointing at content that is not there.

Type of change

  • Bug fix
  • Feature

Testing

Tested environments/configuration:

  • Real 4 MB deck (3 slides, 4 media files, 4,096,364 bytes) and a generated 1.1 MB .docx (40 headings, 160 paragraphs, 10 PNGs), run through the real extractCodeArtifactText with default settings: both produce shells (17,420 B and 7,672 B).
  • Each stored shell rendered alone in headless Chrome (what an old client sees): fallback visible at once.
  • Each shell filled with the file's bytes via fillOfficeFileShell and rendered: deck shows all 3 slides, document renders with all 10 images, fallback hidden, no console errors.
  • Opened the 4 MB deck in the side panel of the running app: real slides shown.
  • Small decks and pictures: a 29 KB deck with one PNG on the inline path renders the picture (1 <img> at natural size), so small decks do not drop pictures today. The 4 MB deck's slide pictures are painted by the renderer without <img> elements; picture fidelity in pptx-preview is out of scope here.

Automated tests:

  • packages/data-provider: npx jest && npx tsc --noEmit — 2208 passed (1 pre-existing skip). New: setting defaults, MB→bytes merge, negative value rejected naming the field, slot filled once and literally.
  • packages/api: npx jest src/files && npx tsc --noEmit — 1168 passed. New: shell building for pptx/docx, inline kept at ≤ 350 KB, trimmed fallback notice, no base64 on the shell path, shell <head> (CDN script src/integrity/crossorigin, CSP) identical to inline and unchanged by filling, a ≥ 4 MB deck's shell ≤ 16 KB without its fallback, every size boundary (350 KB, 350 KB + 1, limit, limit + 1), a yaml limit and a disabled setting reaching routing and the label, bootstraps showing the fallback at once for an empty slot (JSDOM).
  • api: npx jest server/services/Files — 455 passed. New: the merged setting is passed to the extractor and the failure label.
  • client: npx jest src/hooks/Artifacts src/components/Artifacts src/data-provider/Files && npm run typecheck — 213 passed. New: fill with the file bytes, share route, fetch failure keeps the shell, legacy inline untouched, preparing state, a second artifact with an identical shell reports loading instead of the first one's document, and the real hook inside the panel (editor gets the stored shell, preview the filled one).
  • ESLint (--max-warnings=0) and Prettier clean on every changed file.

Screenshots / recordings

No "before" capture: at dev, the same 4 MB deck produces no preview and its card shows "Preview unavailable" (labelled parser-error); a 1.1 MB document shows the plain text outline. The "after" images below are the stored shells rendered in headless Chrome; the running app was checked by hand.

Screenshots (4 MB deck and 1.1 MB document, each filled and shell-only) are being added through the web editor.

Risk / compatibility

  • New config, on by default. fileConfig.officePreview defaults to enabled at 25 MB, so large decks change from "Preview unavailable" to a real preview without any config. enabled: false restores today's routing (inline ≤ 350 KB, text outline up to 2 MB) and labels larger files too-large.
  • Memory. Filling holds the file roughly 1.33× as base64 in the page and again in the preview iframe; bytes are fetched once per open. The server never base64-encodes a shell.
  • Auth. Bytes come through the existing download route, or the share route for shared links; nothing new is exposed inside the sandboxed iframe.
  • Choices worth a look. .potx shares the presentation bucket and takes the shell path, since it is the same OOXML deck format. A large file that hits the render timeout is still labelled timeout, not too-large: too-large is reserved for files over the limit. While bytes load, the panel shows the existing "Preparing preview…" state in place of both tabs.

Checklist

TomasPalsson and others added 27 commits September 28, 2026 16:03
…version

Two cards for the same file across different messages now both render
instead of one message winning the only visible chip. Registration into
the artifact panel now compares update timestamps (falling back to a
mount-order tie-break) so an older card mounting after a newer one, or
remounting after the newer card unmounts, can never clobber the newer
content. Search and shared conversation views now scope every rendered
part to its own message so the same fix applies there.
Extends the JSDOM pptx bootstrap harness with an options object (native
aspect ratio, renderer install/behavior, a capturable 8s safety-net
timer, and a mutable render-slot width) so it can drive every panel
width, a 30-slide deck with no inner scroll box, a resize-triggered
refit, both 16:9 and 4:3 decks, and each fallback trigger (renderer
missing, renderer throws, empty slide list, empty slide wrappers,
render timeout).

Clears the pptx-preview library's own inline width/background once
slides are wrapped, since JSDOM's getComputedStyle doesn't apply the
existing stylesheet's !important override the way a real browser does,
and restores the 16px spacing between stacked slide blocks now that
they live inside the library's own wrapper box instead of directly
under #lc-render.
…sage

A message that ran a tool twice on the same output file (e.g. rewriting
data.zip) showed a card for every run. mapAttachments now collapses
attachments that share a file identity to their last occurrence before
grouping by tool call, so a repeated file surfaces once, under its
newest run. Non-file attachments (no file_id or filepath) are untouched.
A code-execution diagram rewritten in a later turn now shows a card on
every message holding that file, but opening an older message's card
still displayed the stale content. Each ToolMermaidArtifact now offers
its version to a shared per-file "newest seen" record on mount, and
hands the newest entry to Mermaid for registration while keeping its
own inline render unchanged. Corrects two comments claiming shared
conversation views mount with no message context, which Share/Message
already provides.
… copy over a later unlinked duplicate, and key id-less files by filepath instead of filename
… of the message pipeline

Two id-less attachments that share a display name but live at different
filepaths no longer collapse into one chip in the folded attachment group.
Copilot AI balanced review requested due to automatic review settings September 29, 2026 18:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@TomasPalsson
TomasPalsson marked this pull request as ready for review September 29, 2026 18:37

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants