Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/cwv-check.yaml
Original file line number Diff line number Diff line change
@@ -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'
292 changes: 292 additions & 0 deletions blocks/opportunities/opportunities.js
Original file line number Diff line number Diff line change
@@ -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 = '<p>Failed to load visual opportunities.</p>';
}
});
}

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 = `
<div class="url"><strong>Page:</strong> <a href="${item.url}" target="_blank">${item.url}</a></div>
`;

// Tags
const tagList = ['Low contrast', 'Too many CTAs', 'Cookie banner overlap', 'Below-the-fold CTA'];
const selectedTags = new Set();
const tagContainer = document.createElement('div');
tagContainer.classList.add('card-tags');

tagList.forEach((tagText) => {
const tag = document.createElement('span');
tag.classList.add('tag');
tag.textContent = tagText;

tag.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent zoom
tag.classList.toggle('selected');
if (selectedTags.has(tagText)) {
selectedTags.delete(tagText);
} else {
selectedTags.add(tagText);
}
});

tagContainer.appendChild(tag);
});

// Buttons
const actions = document.createElement('div');
actions.classList.add('card-actions');

const approveBtn = document.createElement('button');
approveBtn.classList.add('approve-btn');
approveBtn.textContent = '✅ Approve';

const dismissBtn = document.createElement('button');
dismissBtn.classList.add('dismiss-btn');
dismissBtn.textContent = '❌ Dismiss';

approveBtn.addEventListener('click', async (e) => {
e.stopPropagation(); // Prevent zoom
const endpoint = new URL('http://localhost:8001/approve');
endpoint.searchParams.set('url', item.url);
selectedTags.forEach(tag => endpoint.searchParams.append('tag', tag));

try {
await fetch(endpoint.toString(), { method: 'POST' });
card.style.opacity = '0';
card.style.transform = 'scale(0.98)';
setTimeout(() => card.remove(), 200);
} catch (err) {
console.error('Failed to send approval', err);
}
});

dismissBtn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent zoom
card.style.opacity = '0';
card.style.transform = 'scale(0.98)';
setTimeout(() => {
card.remove();
document.querySelector('.load-more-btn')?.click();
}, 200);
});

actions.appendChild(dismissBtn);
actions.appendChild(approveBtn);

card.appendChild(snapshotWrapper);
card.appendChild(caption);
card.appendChild(tagContainer);
card.appendChild(actions);
grid.appendChild(card);

// === Zoom Modal on Card Click ===
card.addEventListener('click', () => {
const modal = document.querySelector('.zoom-modal');
const modalContent = modal.querySelector('.zoom-modal-content');

const clone = card.cloneNode(true);
clone.classList.add('zoomed');
modalContent.innerHTML = '';
modalContent.appendChild(clone);
modal.classList.add('active');
});
});
});
}

// === Create and attach modal to page ===
const modal = document.createElement('div');
modal.classList.add('zoom-modal');
modal.innerHTML = `<div class="zoom-modal-content"></div>`;
document.body.appendChild(modal);

modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('active');
modal.querySelector('.zoom-modal-content').innerHTML = '';
}
});

document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
modal.classList.remove('active');
modal.querySelector('.zoom-modal-content').innerHTML = '';
}
});
Loading