Skip to content
Merged
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
161 changes: 161 additions & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
---
hide:
- toc
---

# Examples

Every job type the SDK can create, live. Each phone is a real campaign — try the task yourself, it's the interface the labelers get — then open the example behind it for the code.

<div class="preview-gallery" markdown="1">

<div class="preview-gallery__card" markdown="1">

## Classification

<div data-preview-embed data-preview-lazy data-preview-campaign="cmp_1SCZysSRxUHIJ5">
<div class="phone-preview">
<div class="phone-preview__notch"></div>
<div class="phone-preview__btn phone-preview__btn--left-top"></div>
<div class="phone-preview__btn phone-preview__btn--left-bot"></div>
<div class="phone-preview__btn phone-preview__btn--right"></div>
<iframe class="phone-preview__iframe" allow="clipboard-write" title="Live classification preview"></iframe>
</div>
<div class="preview-controls">
<button type="button" data-preview-refresh aria-label="Refresh preview"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg><span>Refresh</span></button>
</div>
</div>

[See the code :octicons-arrow-right-24:](classify_job.md)

</div>

<div class="preview-gallery__card" markdown="1">

## Comparison

<div data-preview-embed data-preview-lazy data-preview-campaign="cmp_1SCZyXpfpHFbmd">
<div class="phone-preview">
<div class="phone-preview__notch"></div>
<div class="phone-preview__btn phone-preview__btn--left-top"></div>
<div class="phone-preview__btn phone-preview__btn--left-bot"></div>
<div class="phone-preview__btn phone-preview__btn--right"></div>
<iframe class="phone-preview__iframe" allow="clipboard-write" title="Live comparison preview"></iframe>
</div>
<div class="preview-controls">
<button type="button" data-preview-refresh aria-label="Refresh preview"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg><span>Refresh</span></button>
</div>
</div>

[See the code :octicons-arrow-right-24:](compare_job.md)

</div>

<div class="preview-gallery__card" markdown="1">

## Locate

<div data-preview-embed data-preview-lazy data-preview-campaign="cmp_1SCZhvwq8dKVfQ">
<div class="phone-preview">
<div class="phone-preview__notch"></div>
<div class="phone-preview__btn phone-preview__btn--left-top"></div>
<div class="phone-preview__btn phone-preview__btn--left-bot"></div>
<div class="phone-preview__btn phone-preview__btn--right"></div>
<iframe class="phone-preview__iframe" allow="clipboard-write" title="Live locate preview"></iframe>
</div>
<div class="preview-controls">
<button type="button" data-preview-refresh aria-label="Refresh preview"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg><span>Refresh</span></button>
</div>
</div>

[See the code :octicons-arrow-right-24:](locate_job.md)

</div>

<div class="preview-gallery__card" markdown="1">

## Draw

<div data-preview-embed data-preview-lazy data-preview-campaign="cmp_1SCZiFks5AUIMi">
<div class="phone-preview">
<div class="phone-preview__notch"></div>
<div class="phone-preview__btn phone-preview__btn--left-top"></div>
<div class="phone-preview__btn phone-preview__btn--left-bot"></div>
<div class="phone-preview__btn phone-preview__btn--right"></div>
<iframe class="phone-preview__iframe" allow="clipboard-write" title="Live draw preview"></iframe>
</div>
<div class="preview-controls">
<button type="button" data-preview-refresh aria-label="Refresh preview"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg><span>Refresh</span></button>
</div>
</div>

[See the code :octicons-arrow-right-24:](draw_job.md)

</div>

<div class="preview-gallery__card" markdown="1">

## Select Words

<div data-preview-embed data-preview-lazy data-preview-campaign="cmp_1SCZiZoHgeFf8t">
<div class="phone-preview">
<div class="phone-preview__notch"></div>
<div class="phone-preview__btn phone-preview__btn--left-top"></div>
<div class="phone-preview__btn phone-preview__btn--left-bot"></div>
<div class="phone-preview__btn phone-preview__btn--right"></div>
<iframe class="phone-preview__iframe" allow="clipboard-write" title="Live select words preview"></iframe>
</div>
<div class="preview-controls">
<button type="button" data-preview-refresh aria-label="Refresh preview"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg><span>Refresh</span></button>
</div>
</div>

[See the code :octicons-arrow-right-24:](select_words_job.md)

</div>

<div class="preview-gallery__card" markdown="1">

## Free Text

<div data-preview-embed data-preview-lazy data-preview-campaign="cmp_1SCZitM8vyAoTC">
<div class="phone-preview">
<div class="phone-preview__notch"></div>
<div class="phone-preview__btn phone-preview__btn--left-top"></div>
<div class="phone-preview__btn phone-preview__btn--left-bot"></div>
<div class="phone-preview__btn phone-preview__btn--right"></div>
<iframe class="phone-preview__iframe" allow="clipboard-write" title="Live free text preview"></iframe>
</div>
<div class="preview-controls">
<button type="button" data-preview-refresh aria-label="Refresh preview"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg><span>Refresh</span></button>
</div>
</div>

[See the code :octicons-arrow-right-24:](free_text_job.md)

</div>

<div class="preview-gallery__card" markdown="1">

## Ranking

<div data-preview-embed data-preview-lazy data-preview-campaign="cmp_1SCZzu6YH6ncAs">
<div class="phone-preview">
<div class="phone-preview__notch"></div>
<div class="phone-preview__btn phone-preview__btn--left-top"></div>
<div class="phone-preview__btn phone-preview__btn--left-bot"></div>
<div class="phone-preview__btn phone-preview__btn--right"></div>
<iframe class="phone-preview__iframe" allow="clipboard-write" title="Live ranking preview"></iframe>
</div>
<div class="preview-controls">
<button type="button" data-preview-refresh aria-label="Refresh preview"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg><span>Refresh</span></button>
</div>
</div>

[See the code :octicons-arrow-right-24:](ranking_job.md)

</div>

</div>

Looking for something else? [Model Ranking](../mri.md) benchmarks AI models on an ongoing leaderboard, and [Ranking Flows](../flows.md) keeps a ranking up to date without full job setup.
151 changes: 149 additions & 2 deletions docs/js/preview-embed.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

const RAPIDS_ORIGIN = 'https://rapids.rapidata.ai';

const LAZY_VISIBLE_RATIO = 0.5;
const FOCUS_GUARD_MS = 4000;
const FOCUS_GUARD_SETTLE_MS = 400;
const FOCUS_JUMP_THRESHOLD_PX = 40;
const FOCUS_GUARD_MAX_CORRECTIONS = 3;

function buildPreviewUrl(campaignId, refreshCount) {
const params = new URLSearchParams({
id: campaignId,
Expand Down Expand Up @@ -114,6 +120,7 @@ function initPreviewEmbed(wrapper) {
const fresh = old.cloneNode(false);
fresh.src = url;
attachIframeLoadHandler(fresh);
keepFocusFromScrollingPage(wrapper, fresh);
old.replaceWith(fresh);
}

Expand All @@ -134,11 +141,151 @@ function initPreviewEmbed(wrapper) {
});
}

// Sync once in case the page loaded with a non-default tab (e.g. deep link).
renderIframe();
// A gallery of embeds would otherwise boot every rapids app at once - each
// one is ~40 requests and close to a megabyte - so opt-in lazy wrappers hold
// off until they are nearly in view. Everything else renders immediately.
if (wrapper.hasAttribute('data-preview-lazy')) {
renderWhenVisible(wrapper, renderIframe);
} else {
// Sync once in case the page loaded with a non-default tab (e.g. deep link).
renderIframe();
}
}

function renderWhenVisible(wrapper, renderIframe) {
if (typeof IntersectionObserver === 'undefined') {
renderIframe();
return;
}

const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
// Hold off until the phone is properly on screen, which keeps the
// autofocus lurch small - see keepFocusFromScrollingPage. Demanding
// near-full visibility instead would be a scroll window only tens of
// pixels wide, which a paging reader skips over entirely, leaving
// the phone never booted at all.
const fitsOnScreen = entry.rootBounds
&& entry.boundingClientRect.height <= entry.rootBounds.height;
if (fitsOnScreen && entry.intersectionRatio < LAZY_VISIBLE_RATIO) return;
observer.unobserve(entry.target);
enqueueLazyRender(wrapper, renderIframe);
});
}, { threshold: [0, LAZY_VISIBLE_RATIO, 1] });

observer.observe(wrapper);
}

// Rapid types with a text answer autofocus their input once the app boots, and
// focusing anything inside an iframe makes the browser scroll the parent
// document to reveal it. Booting is ~a second behind the reader's scroll, so the
// phone is often off-screen by then and the page lurches hundreds of pixels,
// skipping the cards in between. LAZY_VISIBLE_RATIO keeps that lurch small; this
// puts the scroll position back.
//
// A cross-origin iframe taking focus fires no focusin on the parent - the only
// notification is a window blur, which usefully arrives before the scroll.
function keepFocusFromScrollingPage(wrapper, iframe) {
let corrections = 0;
let readerTouchedIt = false;
let expired = false;

// A reader clicking into the phone also blurs the window, and the browser
// scrolling their chosen input into view is wanted, not a lurch to undo.
const noteInteraction = () => { readerTouchedIt = true; };

const stop = () => {
expired = true;
window.removeEventListener('blur', onWindowBlur);
wrapper.removeEventListener('pointerdown', noteInteraction);
};

function onWindowBlur() {
if (readerTouchedIt) return;
const restoreTo = window.scrollY;
const deadline = performance.now() + FOCUS_GUARD_SETTLE_MS;

// Focus lands on the frame a beat after the blur, so poll briefly
// rather than deciding synchronously.
const settle = () => {
if (expired || readerTouchedIt) return;
const jumped = Math.abs(window.scrollY - restoreTo) >= FOCUS_JUMP_THRESHOLD_PX;
if (document.activeElement === iframe && jumped) {
iframe.blur();
window.scrollTo(window.scrollX, restoreTo);
if (++corrections >= FOCUS_GUARD_MAX_CORRECTIONS) stop();
return;
}
if (performance.now() < deadline) requestAnimationFrame(settle);
};
requestAnimationFrame(settle);
}

wrapper.addEventListener('pointerdown', noteInteraction);
window.addEventListener('blur', onWindowBlur);
setTimeout(stop, FOCUS_GUARD_MS);
}

// Several phones can enter the viewport in the same frame. Booting them at once
// means each requests the same rapids bundle before any of them has populated
// the HTTP cache, so the page pays for the bundle once per phone. Serialising
// the boots lets everyone after the first read those chunks from cache.
const lazyRenderQueue = [];
let lazyRenderInFlight = false;
const LAZY_SETTLE_MS = 500;
const LAZY_MAX_WAIT_MS = 3000;

function enqueueLazyRender(wrapper, renderIframe) {
lazyRenderQueue.push({ wrapper, renderIframe });
drainLazyRenderQueue();
}

function drainLazyRenderQueue() {
if (lazyRenderInFlight) return;

const next = lazyRenderQueue.shift();
if (!next) return;

lazyRenderInFlight = true;
next.renderIframe();

const release = () => {
lazyRenderInFlight = false;
drainLazyRenderQueue();
};

// renderIframe swaps in a fresh element, so the iframe to wait on is
// whatever is in the wrapper now.
const iframe = next.wrapper.querySelector('iframe.phone-preview__iframe');
if (!iframe) {
release();
return;
}

let released = false;
const releaseOnce = () => {
if (released) return;
released = true;
clearTimeout(timeout);
release();
};

iframe.addEventListener(
'load',
() => setTimeout(releaseOnce, LAZY_SETTLE_MS),
{ once: true },
);
// A phone that never fires load must not stall the rest of the gallery.
const timeout = setTimeout(releaseOnce, LAZY_MAX_WAIT_MS);
}

function initAllPreviewEmbeds() {
// Instant navigation swaps the whole document; anything still queued belongs
// to the page the reader just left.
lazyRenderQueue.length = 0;
lazyRenderInFlight = false;

document.querySelectorAll('[data-preview-embed]').forEach((wrapper) => {
if (wrapper.dataset.previewEmbedInit === '1') return;
wrapper.dataset.previewEmbedInit = '1';
Expand Down
Loading
Loading