diff --git a/CHANGELOG.md b/CHANGELOG.md index 18cd95d9..c72f76c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.480] — 2026-06-17 — Release QO (inline PDF preview renders all pages) + +### Fixed + +- **The inline PDF preview now renders all pages in a scrollable view instead of just page 1 (#4353).** Previously only the first page of a PDF attachment/export was rendered inline, so multi-page documents looked clipped/broken. The preview now renders every page (capped at 20 for memory) stacked vertically in a scrollable body, shows the page count in the header, and adds a "Showing the first N of M pages — download for the full document" notice when a PDF exceeds the cap. Per-page render failures are isolated (a malformed page is skipped rather than halting the whole preview), and each canvas is attached only after a successful render. Thanks @gourangasatapathyvit. + ## [v0.51.479] — 2026-06-17 — Release QN (named context blocks in the composer) ### Added diff --git a/static/i18n.js b/static/i18n.js index 30fd6946..4b1301d6 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -128,6 +128,7 @@ const LOCALES = { pdf_no_pages: 'PDF has no pages', pdf_error: 'Failed to render PDF preview', pdf_download: 'Download PDF', + pdf_truncated: 'Showing the first {0} of {1} pages — download for the full document', // HTML sandbox preview (#482) html_loading: 'Loading HTML preview…', html_too_large: 'HTML too large for inline preview', @@ -1625,6 +1626,7 @@ const LOCALES = { pdf_no_pages: 'Il PDF non ha pagine', pdf_error: 'Rendering anteprima PDF fallito', pdf_download: 'Scarica PDF', + pdf_truncated: 'Showing the first {0} of {1} pages — download for the full document', // HTML sandbox preview (#482) html_loading: 'Caricamento anteprima HTML…', html_too_large: "HTML troppo grande per l'anteprima inline", @@ -3113,6 +3115,7 @@ const LOCALES = { pdf_no_pages: 'PDF にページがありません', pdf_error: 'PDF プレビューの描画に失敗しました', pdf_download: 'PDF をダウンロード', + pdf_truncated: 'Showing the first {0} of {1} pages — download for the full document', // HTML sandbox preview (#482) html_loading: 'HTML プレビューを読み込み中…', html_too_large: 'HTML が大きすぎてインラインプレビューできません', @@ -5821,6 +5824,7 @@ const LOCALES = { pdf_no_pages: 'Не удалось отобразить предварительный просмотр PDF', pdf_error: 'Не удалось загрузить PDF', pdf_download: 'Скачать PDF', + pdf_truncated: 'Showing the first {0} of {1} pages — download for the full document', html_loading: 'Загрузка предпросмотра HTML…', html_too_large: 'HTML слишком большой для встроенного просмотра', html_error: 'Не удалось загрузить предпросмотр HTML', @@ -7234,6 +7238,7 @@ const LOCALES = { pdf_no_pages: 'No se pudo renderizar la vista previa del PDF', pdf_error: 'Error al cargar el PDF', pdf_download: 'Descargar PDF', + pdf_truncated: 'Showing the first {0} of {1} pages — download for the full document', html_loading: 'Cargando vista previa de HTML…', html_too_large: 'HTML demasiado grande para vista previa', html_error: 'Error al cargar la vista previa de HTML', @@ -8661,6 +8666,7 @@ const LOCALES = { pdf_no_pages: 'PDF-Vorschau konnte nicht gerendert werden', pdf_error: 'PDF konnte nicht geladen werden', pdf_download: 'PDF herunterladen', + pdf_truncated: 'Showing the first {0} of {1} pages — download for the full document', html_loading: 'HTML-Vorschau wird geladen…', html_too_large: 'HTML zu groß für Inline-Vorschau', html_error: 'HTML-Vorschau konnte nicht geladen werden', @@ -10065,6 +10071,7 @@ const LOCALES = { pdf_no_pages: '无法渲染 PDF 预览', pdf_error: 'PDF 加载失败', pdf_download: '下载 PDF', + pdf_truncated: 'Showing the first {0} of {1} pages — download for the full document', html_loading: '正在加载 HTML 预览…', html_too_large: 'HTML 文件过大,无法内联预览', html_error: 'HTML 预览加载失败', @@ -10311,6 +10318,7 @@ const LOCALES = { pdf_no_pages: 'PDF 沒有頁面', pdf_error: 'PDF 預覽轉譯失敗', pdf_download: '下載 PDF', + pdf_truncated: 'Showing the first {0} of {1} pages — download for the full document', // HTML sandbox preview (#482) html_loading: '正在載入 HTML 預覽…', html_too_large: 'HTML 太大,無法直接預覽', @@ -14339,6 +14347,7 @@ const LOCALES = { pdf_no_pages: 'PDF 미리보기를 렌더링할 수 없음', pdf_error: 'PDF 로드 실패', pdf_download: 'PDF 다운로드', + pdf_truncated: 'Showing the first {0} of {1} pages — download for the full document', html_loading: 'HTML 미리보기 로드 중…', html_too_large: 'HTML이 인라인 미리보기에 너무 큼', html_error: 'HTML 미리보기 로드 실패', @@ -14577,6 +14586,7 @@ const LOCALES = { pdf_no_pages: 'Le PDF n\'a pas de pages', pdf_error: 'Échec de l\'affichage de l\'aperçu PDF', pdf_download: 'Télécharger le PDF', + pdf_truncated: 'Showing the first {0} of {1} pages — download for the full document', html_loading: 'Chargement de l\'aperçu HTML…', html_too_large: 'HTML trop volumineux pour l\'aperçu en ligne', html_error: 'Échec de l\'affichage de l\'aperçu HTML', @@ -17250,6 +17260,7 @@ const LOCALES = { pdf_no_pages: 'PDF\'de sayfa yok', pdf_error: 'PDF önizlemesi oluşturulamadı', pdf_download: 'PDF\'yi indir', + pdf_truncated: 'Showing the first {0} of {1} pages — download for the full document', html_loading: 'HTML önizlemesi yükleniyor\u2026', html_too_large: 'HTML satır içi önizleme için çok büyük', html_error: 'HTML önizlemesi oluşturulamadı', @@ -17484,6 +17495,7 @@ const LOCALES = { pdf_no_pages: 'Plik PDF nie ma stron', pdf_error: 'Nie udało się wyrenderować podglądu PDF', pdf_download: 'Pobierz PDF', + pdf_truncated: 'Showing the first {0} of {1} pages — download for the full document', // HTML sandbox preview (#482) html_loading: 'Ładowanie podglądu HTML…', html_too_large: 'Plik HTML jest zbyt duży, aby wyświetlić jego podgląd', diff --git a/static/style.css b/static/style.css index 5ffdd79e..64b63dbc 100644 --- a/static/style.css +++ b/static/style.css @@ -5293,8 +5293,9 @@ main.main > .main-view:not([id="mainChat"]):not([id="mainSettings"]) .main-view- .pdf-preview-header{display:flex;justify-content:space-between;align-items:center;padding:6px 10px;background:var(--surface-2);font-size:12px;font-weight:600;} .pdf-preview-header a{color:var(--accent);text-decoration:none;font-weight:500;} .pdf-preview-header a:hover{text-decoration:underline;} -.pdf-preview-body{display:flex;justify-content:center;padding:8px;background:#fff;border-radius:0 0 6px 6px;max-height:500px;overflow:hidden;} +.pdf-preview-body{display:flex;flex-direction:column;align-items:center;gap:8px;justify-content:flex-start;padding:8px;background:#fff;border-radius:0 0 6px 6px;max-height:600px;overflow:auto;} .pdf-preview-canvas{max-width:100%;height:auto;border-radius:2px;} +.pdf-preview-truncated{font-size:12px;color:var(--muted);text-align:center;padding:4px 0;} .pdf-preview-fallback{padding:8px;font-size:13px;} .pdf-preview-spinner{animation:pulse 1.5s ease-in-out infinite;} @keyframes pulse{0%,100%{opacity:0.4;}50%{opacity:1;}} diff --git a/static/ui.js b/static/ui.js index 49a19253..b50da436 100644 --- a/static/ui.js +++ b/static/ui.js @@ -12436,25 +12436,43 @@ function loadPdfInline(container){ }) .then(pdf=>{ if(!pdf) return; - pdf.getPage(1).then(page=>{ - const canvas=document.createElement('canvas'); - const scale=1.5; - const viewport=page.getViewport({scale}); - canvas.width=viewport.width; - canvas.height=viewport.height; - canvas.className='pdf-preview-canvas'; - page.render({canvasContext:canvas.getContext('2d'),viewport}).promise.then(()=>{ - // Canvas bitmap is runtime state, not part of HTML serialization. - // Attach the canvas as a DOM node — interpolating its serialized - // form into a template string parses back as an empty canvas. - const dlUrl='api/media?path='+encodeURIComponent(path)+'&download=1'; - const wrap=document.createElement('div'); - wrap.className='pdf-preview-wrap'; - wrap.innerHTML=`