489 lines
21 KiB
JavaScript
489 lines
21 KiB
JavaScript
(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); });
|
||
}());
|