import asyncio import os from playwright.async_api import async_playwright from PIL import Image from fpdf import FPDF OUTPUT_DIR = "static/outputs" async def capture_workflows(public_url: str, pdf_filename: str = "workflow_screens.pdf"): os.makedirs(OUTPUT_DIR, exist_ok=True) pdf_path = os.path.join(OUTPUT_DIR, pdf_filename) async with async_playwright() as p: browser = await p.chromium.launch(headless=True) page = await browser.new_page() print(f"Opening page: {public_url}") await page.goto(public_url, wait_until="load") # --- Utility: wait for stable layout --- async def wait_for_layout_stable(): await page.evaluate(""" (async () => { let stableCount = 0; let lastHeight = document.body.scrollHeight; await new Promise((resolve) => { const check = setInterval(() => { const current = document.body.scrollHeight; if (Math.abs(current - lastHeight) < 1) { stableCount++; if (stableCount >= 5) { clearInterval(check); setTimeout(resolve, 300); } } else { stableCount = 0; lastHeight = current; } }, 100); }); })(); """) # --- Fix only sidebar; keep top bar dynamic per screen --- async def fix_layout_dynamically(): try: await page.evaluate(""" const sidebar = document.querySelector('aside, .sidebar, nav'); const topbar = document.querySelector('.top-bar, header, .app-header, .navbar, .page-header'); // Reset old fixed styles [sidebar, topbar].forEach(el => { if (!el) return; el.style.position = ''; el.style.top = ''; el.style.left = ''; el.style.width = ''; el.style.height = ''; el.style.zIndex = ''; el.style.overflow = ''; el.style.transition = ''; }); // Fix sidebar only (topbar remains dynamic) if (sidebar) { const rect = sidebar.getBoundingClientRect(); const sidebarWidth = rect.width || sidebar.offsetWidth || 220; const topOffset = topbar ? topbar.offsetHeight || 60 : 60; sidebar.style.position = 'fixed'; sidebar.style.top = topOffset + 'px'; sidebar.style.left = '0'; sidebar.style.height = `calc(100vh - ${topOffset}px)`; sidebar.style.width = sidebarWidth + 'px'; sidebar.style.zIndex = '1000'; sidebar.style.overflow = 'auto'; sidebar.style.transition = 'none'; sidebar.dataset.locked = 'true'; } const main = document.querySelector('main, .main-content, .app-body, .content'); if (main && sidebar) { main.style.marginLeft = sidebar.offsetWidth + 'px'; main.style.marginTop = topbar ? (topbar.offsetHeight || 60) + 'px' : '0'; main.style.position = 'relative'; } window.scrollTo(0, 0); """) except Exception as e: print(f"[WARN] Layout fix failed: {e}") # --- Wait until sidebar exists --- await page.wait_for_selector( "aside, .sidebar, nav, .side-panel, .left-menu, .menu", timeout=20000 ) await wait_for_layout_stable() await fix_layout_dynamically() await asyncio.sleep(1) # --- JS Logic: detect workflows and tabs dynamically --- js_logic = """ (function(){ const menus=[...document.querySelectorAll('.menu-item')]; const screens=[...document.querySelectorAll('.screen')]; window.__ordered=[]; const seen=new Set(); for(const m of menus){ let id=m.dataset.screen||m.dataset.target; if(!id){ const href=m.getAttribute('href'); if(href && href.startsWith('#')) id=href.substring(1); } const s=screens.find(x=>x.id===id); if(s && !seen.has(s)){seen.add(s);window.__ordered.push({menu:m,screen:s});} } for(const s of screens) if(!seen.has(s)) window.__ordered.push({menu:null,screen:s}); window.__visitedWorkflows=[]; window.__currentIndex=0; window.__done=false; window.__getSubScreens = function(screen){ const tabs=[...screen.querySelectorAll('.tab, .nav-link, [role="tab"], [data-tab], .sub-tab, .tab-item')]; const list=[]; for(const t of tabs){ const sub=t.textContent?.trim(); if(sub && !list.includes(sub)) list.push(sub); } return list; }; window.__captureNext=async function(){ if(window.__done) return false; if(window.__currentIndex>=window.__ordered.length){window.__done=true;return false;} const pair=window.__ordered[window.__currentIndex]; const {menu,screen}=pair; if(!screen){window.__done=true;return false;} const wfName=screen.id || screen.getAttribute('data-name') || ('screen_'+window.__currentIndex); if(window.__visitedWorkflows.includes(wfName)){ window.__currentIndex++; return window.__captureNext(); } window.__visitedWorkflows.push(wfName); document.querySelectorAll('.screen').forEach(s=>s.classList.remove('active')); document.querySelectorAll('.menu-item').forEach(m=>m.classList.remove('active')); screen.classList.add('active'); if(menu) menu.classList.add('active'); screen.scrollIntoView({behavior:'smooth',block:'center'}); window.__currentIndex++; const subs = window.__getSubScreens(screen); return {screenName:wfName, subScreens:subs}; }; window.__clickSubScreen = async function(name){ const tabs=[...document.querySelectorAll('.tab, .nav-link, [role="tab"], [data-tab], .sub-tab, .tab-item')]; const t=tabs.find(x=>x.textContent.trim()===name); if(t){t.click(); return true;} return false; }; })(); """ await page.evaluate(js_logic) await asyncio.sleep(1) screenshots = [] index = 0 # --- Capture all screens --- while True: result = await page.evaluate("window.__captureNext()") if not result: break screen_name = result.get("screenName", f"screen_{index}") sub_screens = result.get("subScreens", []) screenshot_path = os.path.join(OUTPUT_DIR, f"{screen_name}.png") print(f"๐Ÿ“ธ Capturing main screen: {screen_name}") await wait_for_layout_stable() await fix_layout_dynamically() await asyncio.sleep(1.0) await page.screenshot(path=screenshot_path, full_page=True) screenshots.append(screenshot_path) # --- Capture sub-tabs --- first_active_skipped = False for sub in sub_screens: is_active = await page.evaluate( """(subText) => { const tabs=[...document.querySelectorAll('.tab, .nav-link, [role="tab"], [data-tab], .sub-tab, .tab-item')]; const t=tabs.find(x=>x.textContent.trim()===subText); if(!t) return false; const cls=t.getAttribute('class')||''; return cls.includes('active'); }""", sub ) if is_active and not first_active_skipped: first_active_skipped = True continue print(f" โ†ณ Capturing sub-screen: {sub}") await page.evaluate(f"window.__clickSubScreen('{sub}')") await wait_for_layout_stable() await fix_layout_dynamically() await asyncio.sleep(1.0) sub_name_clean = sub.replace(" ", "_").lower() sub_path = os.path.join(OUTPUT_DIR, f"{screen_name}_{sub_name_clean}.png") await page.screenshot(path=sub_path, full_page=True) screenshots.append(sub_path) index += 1 await browser.close() # --- Generate PDF --- if not screenshots: raise RuntimeError("No screenshots captured โ€” check if .screen elements exist!") print(f"๐Ÿงพ Combining {len(screenshots)} screenshots into PDF: {pdf_path}") pdf = FPDF() for img_path in screenshots: image = Image.open(img_path) w, h = image.size pdf_w, pdf_h = 210, 297 aspect = h / w pdf.add_page() pdf.image(img_path, 0, 0, pdf_w, pdf_w * aspect) pdf.output(pdf_path, "F") print(f"โœ… PDF generated successfully: {pdf_path}") return pdf_path generate_ui_report = capture_workflows