diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index f889a0d..a837674 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.207 強化 `/observability/ppt_audit_history` 同頁線上預覽:所有可預覽簡報按鈕改為開啟頁內 PDF 預覽抽屜,保留開新頁與下載,降低產線頁來回跳轉成本並改善手機操作。 - V10.205 補 `/observability/ppt_audit_history` 本頁批次 PDF 預熱:Preview Workbench 可一鍵預熱頁面上尚未快取的 PPTX,沿用單檔 JSON 端點逐一建立 PDF 快取並即時更新狀態。 - V10.203 補 `/observability/ppt_audit_history` 單檔 PDF 預熱操作:未快取的可預覽 PPTX 會顯示「預熱 PDF」,透過 JSON 端點建立 PDF 快取並即時更新頁面狀態。 - V10.201 強化 `/observability/ppt_audit_history` 線上預覽可診斷性:產線清單不觸發轉檔即可顯示 PDF 預覽快取狀態,Pipeline Health、Preview Workbench 與已產檔案表同步標記「PDF 快取 / 首次轉檔」。 diff --git a/templates/admin/ppt_audit_history.html b/templates/admin/ppt_audit_history.html index 5c7271e..0261744 100644 --- a/templates/admin/ppt_audit_history.html +++ b/templates/admin/ppt_audit_history.html @@ -124,7 +124,16 @@
{% if f.file_exists and f.is_valid_ppt %} - + 線上預覽 {% if not f.preview_cache_ready %} @@ -196,7 +205,16 @@
{{ item.status_label }} {% if item.filename and lane.key in ['preview', 'database', 'triage'] %} - + 預覽 {% endif %} @@ -312,7 +330,16 @@ {{ run.status_label }} {{ run.started_at }}{% if run.finished_at %} → {{ run.finished_at }}{% endif %} {% if run.file_name %} - + 預覽 {% endif %} @@ -450,7 +477,16 @@
{% if f.file_exists %} {% if f.is_valid_ppt %} - + 線上預覽 {% if not f.preview_cache_ready %} @@ -569,6 +605,34 @@

Ollama 優先策略 v5.0 — PPT 視覺 QA 產線

+ + diff --git a/tests/test_admin_observability_routes.py b/tests/test_admin_observability_routes.py index fbd5931..c42c1e3 100644 --- a/tests/test_admin_observability_routes.py +++ b/tests/test_admin_observability_routes.py @@ -279,6 +279,9 @@ def test_ppt_audit_history_shows_recent_preview_workbench(client, monkeypatch, t assert '最近可預覽簡報' in html assert 'ocbot_daily_20260517.pptx' in html assert '線上預覽' in html + assert 'data-ppt-open-preview' in html + assert 'data-ppt-preview-modal' in html + assert 'data-ppt-preview-pdf' in html assert 'PDF 快取' in html assert '1 份 PDF 快取' in html diff --git a/web/static/css/page-ppt-audit-history.css b/web/static/css/page-ppt-audit-history.css index 7d7eafb..c94f493 100644 --- a/web/static/css/page-ppt-audit-history.css +++ b/web/static/css/page-ppt-audit-history.css @@ -199,6 +199,110 @@ overflow-wrap: anywhere; } +.ppt-preview-modal[hidden] { + display: none; +} + +body.ppt-preview-open { + overflow: hidden; +} + +.ppt-preview-modal { + position: fixed; + inset: 0; + z-index: 1080; + display: grid; + place-items: center; + padding: var(--momo-space-4, 16px); +} + +.ppt-preview-backdrop { + position: absolute; + inset: 0; + background: rgba(42, 37, 32, 0.48); + backdrop-filter: blur(6px); +} + +.ppt-preview-dialog { + position: relative; + z-index: 1; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + width: min(1180px, 100%); + height: min(82vh, 860px); + border: 1px solid var(--obs-line); + border-radius: var(--momo-radius-lg, 8px); + background: + radial-gradient(circle, rgba(45, 40, 32, 0.08) 1px, transparent 1.2px), + var(--obs-paper); + background-size: 10px 10px, auto; + box-shadow: 0 28px 80px rgba(42, 37, 32, 0.24); + overflow: hidden; +} + +.ppt-preview-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--momo-space-3, 12px); + padding: var(--momo-space-4, 16px); + border-bottom: 1px solid var(--obs-line); + background: rgba(255, 255, 255, 0.62); +} + +.ppt-preview-head h2 { + margin: 0 0 var(--momo-space-1, 4px); + color: var(--obs-ink); + font-size: var(--momo-text-title, 18px); + font-weight: var(--momo-font-weight-black, 800); + letter-spacing: 0; + overflow-wrap: anywhere; +} + +.ppt-preview-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--momo-space-2, 8px); + flex-wrap: wrap; +} + +.ppt-preview-frame-wrap { + position: relative; + min-height: 0; + background: rgba(255, 255, 255, 0.52); +} + +.ppt-preview-frame-wrap iframe { + position: relative; + z-index: 1; + width: 100%; + height: 100%; + min-height: 560px; + border: 0; + background: var(--obs-paper); +} + +.ppt-preview-loading { + position: absolute; + inset: var(--momo-space-4, 16px); + z-index: 0; + display: grid; + place-items: center; + border: 1px dashed var(--obs-line); + border-radius: var(--momo-radius-md, 6px); + color: var(--obs-muted); + font-family: var(--momo-font-mono); + background: + radial-gradient(circle, rgba(45, 40, 32, 0.08) 1px, transparent 1.2px), + rgba(255, 255, 255, 0.42); + background-size: 10px 10px, auto; +} + +.ppt-preview-frame-wrap.is-loaded .ppt-preview-loading { + display: none; +} + .ppt-health-board { display: grid; grid-template-columns: minmax(280px, 0.36fr) minmax(0, 0.64fr); @@ -800,10 +904,30 @@ .ppt-table-title, .ppt-workbench-head, .ppt-workbench-actions, + .ppt-preview-head, .ppt-run-log-head, .ppt-run-row, .ppt-coverage-row { flex-direction: column; align-items: flex-start; } + + .ppt-preview-modal { + padding: 0; + } + + .ppt-preview-dialog { + width: 100%; + height: 100%; + max-height: none; + border-radius: 0; + } + + .ppt-preview-actions { + justify-content: flex-start; + } + + .ppt-preview-frame-wrap iframe { + min-height: calc(100vh - 168px); + } } diff --git a/web/static/js/observability-charts.js b/web/static/js/observability-charts.js index 4144226..f4a5d77 100644 --- a/web/static/js/observability-charts.js +++ b/web/static/js/observability-charts.js @@ -596,6 +596,71 @@ function initPptAutoGeneration() { const panel = document.querySelector('[data-ppt-auto-generation]'); const pageStatus = document.querySelector('[data-ppt-auto-status]'); + const previewModal = document.querySelector('[data-ppt-preview-modal]'); + const previewFrame = previewModal ? previewModal.querySelector('[data-ppt-preview-frame]') : null; + const previewTitle = previewModal ? previewModal.querySelector('[data-ppt-preview-title]') : null; + const previewFilename = previewModal ? previewModal.querySelector('[data-ppt-preview-filename]') : null; + const previewOpenPage = previewModal ? previewModal.querySelector('[data-ppt-preview-open-page]') : null; + const previewDownload = previewModal ? previewModal.querySelector('[data-ppt-preview-download]') : null; + const previewLoading = previewModal ? previewModal.querySelector('[data-ppt-preview-loading]') : null; + const previewFrameWrap = previewModal ? previewModal.querySelector('.ppt-preview-frame-wrap') : null; + + function closePreviewModal() { + if (!previewModal) return; + previewModal.hidden = true; + previewModal.setAttribute('aria-hidden', 'true'); + document.body.classList.remove('ppt-preview-open'); + if (previewFrame) { + previewFrame.removeAttribute('src'); + } + } + + function openPreviewModal(trigger) { + if (!previewModal || !previewFrame) return false; + const pdfUrl = trigger.dataset.pptPreviewPdf || ''; + if (!pdfUrl) return false; + const title = trigger.dataset.pptPreviewTitle || '簡報線上預覽'; + const filename = trigger.dataset.pptFilename || ''; + if (previewTitle) previewTitle.textContent = title; + if (previewFilename) previewFilename.textContent = filename; + if (previewOpenPage) previewOpenPage.href = trigger.dataset.pptPreviewPage || trigger.href || pdfUrl; + if (previewDownload) previewDownload.href = trigger.dataset.pptDownloadUrl || '#'; + if (previewLoading) previewLoading.textContent = filename ? `${filename} 正在載入 PDF 預覽` : '正在載入 PDF 預覽'; + previewModal.hidden = false; + previewModal.setAttribute('aria-hidden', 'false'); + document.body.classList.add('ppt-preview-open'); + if (previewFrameWrap) previewFrameWrap.classList.remove('is-loaded'); + previewFrame.src = pdfUrl; + return true; + } + + if (previewModal) { + if (previewFrame && previewFrame.dataset.boundLoad !== '1') { + previewFrame.dataset.boundLoad = '1'; + previewFrame.addEventListener('load', () => { + if (previewFrameWrap) previewFrameWrap.classList.add('is-loaded'); + }); + } + previewModal.querySelectorAll('[data-ppt-preview-close]').forEach(button => { + if (button.dataset.bound === '1') return; + button.dataset.bound = '1'; + button.addEventListener('click', closePreviewModal); + }); + document.addEventListener('keydown', event => { + if (event.key === 'Escape' && !previewModal.hidden) closePreviewModal(); + }); + } + + document.querySelectorAll('[data-ppt-open-preview]').forEach(link => { + if (link.dataset.bound === '1') return; + link.dataset.bound = '1'; + link.addEventListener('click', event => { + if (openPreviewModal(link)) { + event.preventDefault(); + } + }); + }); + document.querySelectorAll('[data-ppt-aider-heal]').forEach(button => { if (button.dataset.bound === '1') return; button.dataset.bound = '1';