(function() { var STEP_ORDER = ['operation', 'test', 'publish_operation', 'agent', 'key', 'mcp_connection', 'first_call']; var state = { epoch: 0, workspaceId: null, snapshot: null, loading: false, error: null, open: false, dismissed: false, controller: null, refreshPromise: null, refreshQueued: false, startedRequested: false, }; var channel = null; var trigger = null; var panel = null; var heading = null; var subtitle = null; var collapseButton = null; var lastFocused = null; function tKey(key) { return window.t ? window.t(key) : key; } function currentWorkspace() { return window.getCurrentWorkspace ? window.getCurrentWorkspace() : null; } function preferenceKey() { return 'crank_onboarding_ui:' + (state.workspaceId || 'none'); } function safePreference() { try { return JSON.parse(localStorage.getItem(preferenceKey())) || {}; } catch (_error) { return {}; } } function writePreference(value) { try { localStorage.setItem(preferenceKey(), JSON.stringify(value)); } catch (_error) {} } function updatePreference(value) { writePreference(Object.assign({}, safePreference(), value)); } function injectStylesheet() { if (document.querySelector('link[data-crank-onboarding-css]')) return; var link = document.createElement('link'); link.rel = 'stylesheet'; link.href = (window.APP_BASE || '/') + 'css/onboarding.css'; link.dataset.crankOnboardingCss = 'true'; document.head.appendChild(link); } function button(label, testId, handler, className) { var node = document.createElement('button'); node.type = 'button'; node.textContent = label; if (testId) node.dataset.testid = testId; node.className = className || 'onboarding-icon-button'; node.addEventListener('click', handler); return node; } function normalizeStep(raw) { var id = raw.id || raw.kind; if (id === 'publish') id = 'publish_operation'; if (id === 'connection') id = 'mcp_connection'; var completed = raw.completed === true || raw.status === 'complete'; return { id: id, completed: completed, status: raw.status || (completed ? 'complete' : 'pending'), actionCode: raw.action_code || raw.action || '', reasonCode: raw.reason_code || '', }; } function normalizedSteps() { var raw = state.snapshot && Array.isArray(state.snapshot.steps) ? state.snapshot.steps : []; var mapped = raw.map(normalizeStep); return STEP_ORDER.map(function(id) { return mapped.find(function(item) { return item.id === id; }) || { id: id, completed: false, status: 'pending', actionCode: '', reasonCode: '' }; }); } function completedCount() { return normalizedSteps().filter(function(step) { return step.completed; }).length; } function currentStep(steps) { return steps.find(function(step) { return !step.completed && step.status === 'current'; }) || steps.find(function(step) { return !step.completed; }) || null; } function stepLabel(id) { return tKey('onboarding.step.' + id); } function actionLabel(id) { return tKey('onboarding.action.' + id); } function deepLink(step) { var snapshot = state.snapshot || {}; var returnTo = encodeURIComponent(window.location.pathname + window.location.search); if (step.id === 'operation') return '/wizard/?onboarding=1&return=' + returnTo; if (step.id === 'test' || step.id === 'publish_operation') { if (!snapshot.operation_id) return '/wizard/?onboarding=1&return=' + returnTo; return '/wizard/?mode=edit&operationId=' + encodeURIComponent(snapshot.operation_id) + '&onboarding=1&step=5&return=' + returnTo; } if (step.id === 'agent') { return '/agents?onboarding=1&action=create&operationId=' + encodeURIComponent(snapshot.operation_id || '') + '&operationVersion=' + encodeURIComponent(snapshot.operation_version || '') + '&return=' + returnTo; } if (step.id === 'key' || step.id === 'mcp_connection') { return '/api-keys?onboarding=1&action=create&agentId=' + encodeURIComponent(snapshot.agent_id || '') + '&agentRevision=' + encodeURIComponent(snapshot.catalog_revision || '') + '&return=' + returnTo; } if (step.id === 'first_call' && snapshot.first_call && snapshot.first_call.log_id) { return '/logs?log_id=' + encodeURIComponent(snapshot.first_call.log_id); } return '/logs?agent_id=' + encodeURIComponent(snapshot.agent_id || ''); } function navigate(step) { window.location.href = deepLink(step); } function recordStarted() { if (state.startedRequested || !state.snapshot || state.snapshot.completed) return; state.startedRequested = true; recordPresentation('started', { stable: true }).then(function(response) { if (!response) state.startedRequested = false; }); } function setOpen(open) { state.open = open; panel.hidden = !open; trigger.setAttribute('aria-expanded', String(open)); if (open) { lastFocused = document.activeElement; render(); var focusTarget = panel.querySelector('[aria-current="step"] .onboarding-step-action') || panel.querySelector('button'); if (focusTarget) focusTarget.focus(); recordStarted(); } else if (lastFocused && typeof lastFocused.focus === 'function') { lastFocused.focus(); } } function render() { if (!trigger || !panel) return; var count = completedCount(); trigger.textContent = state.dismissed ? tKey('onboarding.resume') : tKey('onboarding.title') + ' · ' + count + '/7'; trigger.setAttribute('aria-label', state.dismissed ? tKey('onboarding.resume') : trigger.textContent); if (heading) heading.textContent = tKey('onboarding.title'); if (subtitle) subtitle.textContent = tKey('onboarding.subtitle'); if (collapseButton) collapseButton.setAttribute('aria-label', tKey('onboarding.collapse')); var body = panel.querySelector('.onboarding-body'); var hadActionFocus = state.open && document.activeElement && body.contains(document.activeElement) && document.activeElement.classList.contains('onboarding-step-action'); body.replaceChildren(); var status = document.createElement('div'); status.className = 'onboarding-status'; status.setAttribute('role', 'status'); status.setAttribute('aria-live', 'polite'); status.id = 'crank-onboarding-progress'; status.textContent = state.loading ? tKey('onboarding.loading') : tKey('onboarding.progress').replace('{count}', String(count)); body.appendChild(status); if (state.error) { var error = document.createElement('div'); error.className = 'onboarding-error'; error.setAttribute('role', 'alert'); var message = document.createElement('div'); message.textContent = tKey('onboarding.error'); error.appendChild(message); var errorCode = state.error.code || (state.error.payload && state.error.payload.error && (state.error.payload.error.code || state.error.payload.error.error_code)) || (state.error.payload && (state.error.payload.code || state.error.payload.error_code)); var support = [ errorCode ? tKey('onboarding.error_code') + ': ' + errorCode : '', state.error.requestId ? tKey('onboarding.request_id') + ': ' + state.error.requestId : '', state.error.traceId ? tKey('onboarding.trace_id') + ': ' + state.error.traceId : '', ].filter(Boolean); if (support.length) { var supportIds = document.createElement('div'); supportIds.className = 'onboarding-error-ids'; supportIds.textContent = support.join(' · '); error.appendChild(supportIds); } error.appendChild(button(tKey('onboarding.retry'), 'onboarding-retry', refresh, 'onboarding-step-action')); body.appendChild(error); if (state.focusError && state.open) { state.focusError = false; var retry = error.querySelector('[data-testid="onboarding-retry"]'); if (retry) retry.focus(); } return; } if (!state.snapshot) return; var steps = normalizedSteps(); var active = currentStep(steps); if (state.snapshot.completed || !active) { var complete = document.createElement('div'); complete.className = 'onboarding-completion'; complete.dataset.testid = 'onboarding-completion'; complete.textContent = tKey('onboarding.completed'); var firstCall = state.snapshot.first_call; if (firstCall) { var evidence = document.createElement('dl'); evidence.className = 'onboarding-first-call-evidence'; [ [tKey('onboarding.tool'), firstCall.tool_name], [tKey('onboarding.timestamp'), firstCall.occurred_at], [tKey('onboarding.request_id'), firstCall.request_id], [tKey('onboarding.trace_id'), firstCall.trace_id], ].forEach(function(entry) { if (!entry[1]) return; var term = document.createElement('dt'); term.textContent = entry[0]; var value = document.createElement('dd'); value.textContent = entry[1]; evidence.appendChild(term); evidence.appendChild(value); }); complete.appendChild(evidence); if (firstCall.log_id) { var link = document.createElement('a'); link.href = '/logs?log_id=' + encodeURIComponent(firstCall.log_id); link.textContent = tKey('onboarding.open_invocation'); complete.appendChild(link); } } body.appendChild(complete); } else { var list = document.createElement('ol'); list.className = 'onboarding-list'; list.setAttribute('aria-label', tKey('onboarding.progress_label')); list.setAttribute('aria-describedby', 'crank-onboarding-progress'); steps.forEach(function(step, index) { var item = document.createElement('li'); item.className = 'onboarding-step' + (step.completed ? ' is-complete' : '') + (step.status === 'regressed' ? ' is-regressed' : ''); if (step === active) item.setAttribute('aria-current', 'step'); var marker = document.createElement('span'); marker.className = 'onboarding-step-marker'; marker.textContent = step.completed ? '✓' : String(index + 1); marker.setAttribute('aria-hidden', 'true'); var content = document.createElement('div'); var title = document.createElement('div'); title.className = 'onboarding-step-title'; title.textContent = stepLabel(step.id); content.appendChild(title); if (step === active) { var reason = document.createElement('div'); reason.className = 'onboarding-step-reason'; reason.textContent = tKey(step.status === 'regressed' ? 'onboarding.regressed' : 'onboarding.action_needed'); content.appendChild(reason); content.appendChild(button(actionLabel(step.id), null, function() { navigate(step); }, 'onboarding-step-action')); } item.appendChild(marker); item.appendChild(content); list.appendChild(item); }); body.appendChild(list); } var footer = document.createElement('div'); footer.className = 'onboarding-footer'; footer.appendChild(button(tKey('onboarding.refresh'), 'onboarding-refresh', refresh, 'onboarding-link-button')); footer.appendChild(button(tKey('onboarding.dismiss'), 'onboarding-dismiss', dismiss, 'onboarding-link-button')); body.appendChild(footer); if (hadActionFocus) { var replacementAction = panel.querySelector('[aria-current="step"] .onboarding-step-action'); if (replacementAction) replacementAction.focus(); } } function refresh() { var workspace = currentWorkspace(); var workspaceId = workspace ? workspace.id : null; if (state.refreshPromise && workspaceId === state.workspaceId) { state.refreshQueued = true; return state.refreshPromise; } if (workspaceId !== state.workspaceId) state.refreshQueued = false; var promise = performRefresh(workspaceId); state.refreshPromise = promise; promise.finally(function() { if (state.refreshPromise !== promise) return; state.refreshPromise = null; if (state.refreshQueued) { state.refreshQueued = false; refresh(); } }); return promise; } async function performRefresh(workspaceId) { var workspaceChanged = workspaceId !== state.workspaceId; var epoch = ++state.epoch; if (state.controller) state.controller.abort(); state.controller = typeof AbortController === 'function' ? new AbortController() : null; state.workspaceId = workspaceId; if (workspaceChanged) { state.dismissed = Boolean(safePreference().dismissed); state.startedRequested = false; if (state.open) setOpen(false); } state.loading = true; state.error = null; state.focusError = false; render(); if (!workspaceId || !window.CrankApi || typeof window.CrankApi.getOnboarding !== 'function') { state.loading = false; state.error = new Error(tKey('onboarding.error')); render(); return; } try { var snapshot = await window.CrankApi.getOnboarding(workspaceId); if (epoch !== state.epoch || workspaceId !== (currentWorkspace() && currentWorkspace().id)) return; state.snapshot = snapshot; var preference = safePreference(); if (window.location.pathname === '/' && !preference.opened && !preference.dismissed && !snapshot.completed) { updatePreference({ opened: true }); setOpen(true); } renderDeepLinkRecovery(snapshot); } catch (error) { if (epoch !== state.epoch) return; state.error = error; state.focusError = state.open; } finally { if (epoch === state.epoch) { state.loading = false; render(); } } } async function recordPresentation(eventName, options) { if (!state.snapshot || !state.workspaceId || !window.CrankApi.recordOnboardingEvent) return; var expectedRevision = Number(state.snapshot.revision || 0); var expectedEpoch = state.epoch; var expectedWorkspaceId = state.workspaceId; var key = options && options.stable ? ['ui', eventName, state.workspaceId, 'v1'].join(':') : ['ui', eventName, state.workspaceId, expectedRevision].join(':'); try { var response = await window.CrankApi.recordOnboardingEvent(state.workspaceId, { event: eventName, idempotency_key: key, expected_revision: expectedRevision, }, options && options.keepalive ? { keepalive: true } : null); var currentRevision = Number(state.snapshot && state.snapshot.revision || 0); if (response && expectedEpoch === state.epoch && expectedWorkspaceId === state.workspaceId && expectedWorkspaceId === (currentWorkspace() && currentWorkspace().id) && currentRevision === expectedRevision) { state.snapshot = response; render(); } return response; } catch (error) { if (error.status === 409) await refresh(); return null; } } async function dismiss() { updatePreference({ dismissed: true, opened: true }); state.dismissed = true; setOpen(false); render(); await recordPresentation('dismissed'); await recordPresentation('abandoned', { stable: true }); } function resume() { updatePreference({ dismissed: false, opened: true }); state.dismissed = false; setOpen(true); recordPresentation('resumed'); } function renderDeepLinkRecovery(snapshot) { var params = new URLSearchParams(window.location.search); if (params.get('onboarding') !== '1') return; var stale = false; if (window.location.pathname === '/agents') { var expectedOperation = params.get('operationId') || ''; var expectedVersion = Number(params.get('operationVersion') || 0); stale = Boolean(expectedOperation) && ( snapshot.operation_id !== expectedOperation || (expectedVersion && snapshot.operation_version !== expectedVersion) ); } else if (window.location.pathname === '/api-keys') { var expectedAgent = params.get('agentId') || ''; var expectedRevision = Number(params.get('agentRevision') || 0); stale = Boolean(expectedAgent) && ( snapshot.agent_id !== expectedAgent || (expectedRevision && snapshot.catalog_revision !== expectedRevision) ); } var existing = document.getElementById('onboarding-deep-link-stale'); if (!stale) { if (existing) existing.remove(); return; } if (existing) return; var recovery = document.createElement('div'); recovery.id = 'onboarding-deep-link-stale'; recovery.className = 'onboarding-deep-link-stale'; recovery.setAttribute('role', 'alert'); recovery.dataset.testid = 'onboarding-deep-link-stale'; var message = document.createElement('div'); message.textContent = tKey('onboarding.deep_link_stale'); recovery.appendChild(message); recovery.appendChild(button(tKey('onboarding.reselect'), 'onboarding-reselect', async function() { var resetButton = this; resetButton.disabled = true; try { if (window.CrankApi && typeof window.CrankApi.resetOnboardingSelection === 'function') { await window.CrankApi.resetOnboardingSelection(state.workspaceId, Number(snapshot.revision || 0)); } var clean = new URLSearchParams(window.location.search); ['onboarding', 'action', 'operationId', 'operationVersion', 'agentId', 'agentRevision', 'return'].forEach(function(key) { clean.delete(key); }); window.location.replace(window.location.pathname + (clean.toString() ? '?' + clean.toString() : '')); } catch (error) { resetButton.disabled = false; state.error = error; if (!state.open) setOpen(true); render(); } }, 'onboarding-step-action')); document.body.appendChild(recovery); } function mount() { if (document.getElementById('crank-onboarding-root')) return; injectStylesheet(); var root = document.createElement('div'); root.id = 'crank-onboarding-root'; trigger = button(tKey('onboarding.title'), 'onboarding-trigger', function() { if (state.dismissed) { resume(); return; } setOpen(!state.open); }, 'onboarding-trigger'); trigger.setAttribute('aria-controls', 'crank-onboarding-panel'); trigger.setAttribute('aria-expanded', 'false'); panel = document.createElement('aside'); panel.id = 'crank-onboarding-panel'; panel.className = 'onboarding-panel'; panel.dataset.testid = 'onboarding-checklist'; panel.setAttribute('aria-labelledby', 'crank-onboarding-title'); panel.hidden = true; var head = document.createElement('div'); head.className = 'onboarding-head'; var headingWrap = document.createElement('div'); heading = document.createElement('h2'); heading.id = 'crank-onboarding-title'; heading.className = 'onboarding-title'; heading.textContent = tKey('onboarding.title'); subtitle = document.createElement('p'); subtitle.className = 'onboarding-subtitle'; subtitle.textContent = tKey('onboarding.subtitle'); headingWrap.appendChild(heading); headingWrap.appendChild(subtitle); var actions = document.createElement('div'); actions.className = 'onboarding-head-actions'; collapseButton = button('−', 'onboarding-collapse', function() { setOpen(false); }, 'onboarding-icon-button'); collapseButton.setAttribute('aria-label', tKey('onboarding.collapse')); actions.appendChild(collapseButton); head.appendChild(headingWrap); head.appendChild(actions); var body = document.createElement('div'); body.className = 'onboarding-body'; panel.appendChild(head); panel.appendChild(body); root.appendChild(trigger); root.appendChild(panel); document.body.appendChild(root); refresh(); } function signalRefresh() { if (channel) channel.postMessage({ type: 'refresh' }); try { localStorage.setItem('crank_onboarding_signal', String(Date.now())); } catch (_error) {} } window.CrankOnboarding = { refresh: refresh, signalRefresh: signalRefresh }; document.addEventListener('DOMContentLoaded', async function() { await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve()); mount(); if (typeof BroadcastChannel === 'function') { channel = new BroadcastChannel('crank-onboarding'); channel.addEventListener('message', function(event) { if (event && event.data && event.data.type !== 'refresh') return; refresh(); }); } }); window.addEventListener('crank:workspacechange', function() { state.snapshot = null; refresh(); }); window.addEventListener('crank:sessionchange', refresh); window.addEventListener('crank:langchange', function() { render(); }); window.addEventListener('storage', function(event) { if (event.key === 'crank_onboarding_signal') refresh(); }); document.addEventListener('visibilitychange', function() { if (!document.hidden) refresh(); }); document.addEventListener('keydown', function(event) { if (event.key === 'Escape' && state.open) setOpen(false); }); }());