diff --git a/.github/workflows/cwv-check.yaml b/.github/workflows/cwv-check.yaml new file mode 100644 index 00000000..1a706ebf --- /dev/null +++ b/.github/workflows/cwv-check.yaml @@ -0,0 +1,55 @@ +name: CWV PR Check + +on: + pull_request: + types: [opened, synchronize] + +jobs: + run-cwv-check: + runs-on: ubuntu-latest + environment: CWV + env: + AZURE_OPENAI_API_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_API_DEPLOYMENT_NAME }} + AZURE_OPENAI_API_INSTANCE_NAME: ${{ secrets.AZURE_OPENAI_API_INSTANCE_NAME }} + AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} + AZURE_OPENAI_API_VERSION: ${{ secrets.AZURE_OPENAI_API_VERSION }} + GOOGLE_CRUX_API_KEY: ${{ secrets.GOOGLE_CRUX_API_KEY }} + GOOGLE_PAGESPEED_INSIGHTS_API_KEY: ${{ secrets.GOOGLE_PAGESPEED_INSIGHTS_API_KEY }} + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install CWV Agent + run: npm install --no-save git+https://github.com/ramboz/cwv-agent.git + + - name: Build Preview URL + id: build_url + run: | + BRANCH=${{ github.head_ref }} + REPO=${{ github.event.repository.name }} + OWNER=${{ github.repository_owner }} + PREVIEW_URL="https://${BRANCH}--${REPO}--${OWNER}.aem.page" + echo "PREVIEW_URL=$PREVIEW_URL" >> $GITHUB_OUTPUT + + - name: Run CWV Agent + run: | + node node_modules/cwv-agent/index.js --action prompt --url ${{ steps.build_url.outputs.PREVIEW_URL }} --device mobile --model gpt-4o + node node_modules/cwv-agent/index.js --action prompt --url ${{ steps.build_url.outputs.PREVIEW_URL }} --device desktop --model gpt-4o + + - name: Upload All CWV Summary Reports + uses: actions/upload-artifact@v4 + with: + name: cwv-summaries + path: .cache/*.report.gpt4o.summary.md + + - name: Print project file tree (excluding node_modules) + run: | + sudo apt-get update && sudo apt-get install -y tree + echo "📁 Project structure (excluding node_modules):" + tree -L 3 -a -I 'node_modules' \ No newline at end of file diff --git a/blocks/opportunities/opportunities.js b/blocks/opportunities/opportunities.js new file mode 100644 index 00000000..d77786b0 --- /dev/null +++ b/blocks/opportunities/opportunities.js @@ -0,0 +1,292 @@ +import { createOptimizedPicture } from '../../scripts/lib-franklin.js'; + +export async function saveSnapshot(sessionId, data) { + try { + const json = JSON.stringify(data); + sessionStorage.setItem(`snapshot:${sessionId}`, json); + console.log(`Snapshot saved in sessionStorage: snapshot:${sessionId}`); + } catch (err) { + console.error('❌ Failed to save snapshot:', err); + } +} + +export async function loadSnapshot(sessionId) { + try { + const raw = sessionStorage.getItem(`snapshot:${sessionId}`); + if (!raw) throw new Error('No snapshot found'); + return JSON.parse(raw); + } catch (err) { + console.error('❌ Failed to load snapshot:', err); + return null; + } +} + +export default async function decorate(block) { + const params = new URLSearchParams(window.location.search); + const domain = params.get('domain') || 'default-domain'; + const domainkey = params.get('domainkey') || 'default-domainkey'; + const startdate = params.get('startdate') || 'default-date' + const enddate = params.get('enddate') || 'default-date' + + block.innerHTML = ''; + + // === Create loader (spinner only) === + const loader = document.createElement('div'); + loader.classList.add('loading-indicator'); + loader.style.display = 'none'; + + // === Create grid === + const grid = document.createElement('div'); + grid.classList.add('gallery-grid'); + block.appendChild(grid); + + // === Create "More" button === + const moreBtn = document.createElement('button'); + moreBtn.textContent = 'More Opportunities'; + moreBtn.classList.add('load-more-btn'); + moreBtn.style.display = 'none'; + + block.appendChild(loader); + block.appendChild(moreBtn); // append after loader so loader can replace it + + // === API Setup === + const apiURL = new URL('http://localhost:8001/get-bboxes/start'); + apiURL.searchParams.set('domain', domain); + apiURL.searchParams.set('checkpoint', 'click'); + apiURL.searchParams.set('domainkey', domainkey); + apiURL.searchParams.set('startdate', startdate) + apiURL.searchParams.set('enddate', enddate) + + + let sessionId = null; + let cursor = 0; + let total = 0; + + requestAnimationFrame(async () => { + try { + loader.style.display = 'block'; + moreBtn.style.display = 'none'; + + const res = await fetch(apiURL.toString()); + const data = await res.json(); + + + sessionId = data.sessionId; + cursor = data.cursor; + total = data.total; + const results = data.result || []; + + if (results.length === 0) { + block.appendChild(document.createTextNode('No opportunities found.')); + loader.remove(); + moreBtn.remove(); + return; + } + + renderResults(results, grid); + + loader.style.display = 'none'; + moreBtn.style.display = 'block'; + + moreBtn.addEventListener('click', async () => { + if (cursor >= total) { + moreBtn.disabled = true; + moreBtn.textContent = 'No more opportunities'; + return; + } + + moreBtn.replaceWith(loader); + loader.style.display = 'block'; + + const nextURL = new URL('http://localhost:8001/get-bboxes/next'); + + try { + const nextRes = await fetch(nextURL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + data: data.raw, // ← the full data array + cursor, // ← current offset + domain, // ← e.g. "wilson" + checkpoint: 'click', // ← e.g. "click" + domainkey, // ← whatever your key is + }) + }); + + const nextData = await nextRes.json(); + const nextResults = nextData.result || []; + + if (nextResults.length === 0) { + loader.remove(); + return; + } + + renderResults(nextResults, grid); + cursor = nextData.cursor; + + loader.replaceWith(moreBtn); + moreBtn.style.display = 'block'; + + if (cursor >= total) { + moreBtn.disabled = true; + moreBtn.textContent = 'No more opportunities'; + } + } catch (err) { + console.error('📸 Failed to load next batch:', err); + loader.remove(); + } + }); + } catch (err) { + console.error('📸 Failed to load snapshots:', err); + block.innerHTML = '
Failed to load visual opportunities.
'; + } + }); +} + +function renderResults(results, grid) { + results.forEach((result) => { + const sources = result?.[0]?.sources || result?.sources || []; + sources.forEach((item) => { + const card = document.createElement('div'); + card.classList.add('gallery-card'); + + if (item.mobile_snapshots) { + card.classList.add('mobile'); + } + + const snapshotWrapper = document.createElement('div'); + snapshotWrapper.classList.add('gallery-image-wrapper'); + + if (item.mobile_snapshots) { + const elementImg = document.createElement('img'); + elementImg.src = item.mobile_snapshots.element_snapshot; + elementImg.alt = `Element Snapshot`; + elementImg.loading = 'lazy'; + snapshotWrapper.appendChild(elementImg); + + const viewportImg = document.createElement('img'); + viewportImg.src = item.mobile_snapshots.viewport_snapshot; + viewportImg.alt = `Viewport Snapshot`; + viewportImg.loading = 'lazy'; + snapshotWrapper.appendChild(viewportImg); + } else { + const img = document.createElement('img'); + img.src = item.snapshot; + img.alt = `Snapshot`; + img.loading = 'lazy'; + snapshotWrapper.appendChild(img); + } + + const caption = document.createElement('div'); + caption.classList.add('gallery-card-details'); + caption.innerHTML = ` +