[eric] onboarding flow, user ui/ux

This commit is contained in:
ciregenz
2026-04-05 00:19:36 -07:00
parent a82059e925
commit e135dff436
5 changed files with 109 additions and 4 deletions
@@ -3,6 +3,7 @@ import { Box, Typography, Modal, Button, CircularProgress, TextField } from '@mu
import { useAppSelector } from '@/shared/hooks';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { API_BASE } from '@/shared/config';
import { trackEvent } from '@/shared/analytics';
const SUBSCRIPTION_PROVIDERS = [
{ id: 'claude', name: 'Claude', desc: 'Sonnet, Opus, Haiku', color: '#E8927A', preview: false },
@@ -81,6 +82,7 @@ const OnboardingModal: React.FC = () => {
if (nineRouterReady === null) return; // still checking
setOpen(true);
trackEvent('onboarding.started', { step: 'profile' });
}, [nineRouterReady]);
// Cleanup timers on unmount
@@ -109,6 +111,7 @@ const OnboardingModal: React.FC = () => {
if (dashboard?.id) {
const seedRes = await fetch(`${API_BASE}/dashboards/${dashboard.id}/seed-demo`, { method: 'POST' });
if (seedRes.ok) {
trackEvent('onboarding.completed', { dashboard_id: dashboard.id });
localStorage.setItem('openswarm_walkthrough_pending', 'true');
setOpen(false);
// Force full page load to ensure dashboard mounts fresh with walkthrough
@@ -142,7 +145,14 @@ const OnboardingModal: React.FC = () => {
}),
});
} catch {}
trackEvent('onboarding.profile_submitted', {
has_name: !!userName.trim(),
has_email: !!userEmail.trim(),
use_cases: useCases,
use_cases_count: useCases.length,
});
setStep('connect');
trackEvent('onboarding.connect_started', { nine_router_ready: nineRouterReady });
};
// Same connect logic as Settings/SubscriptionCards
@@ -151,6 +161,7 @@ const OnboardingModal: React.FC = () => {
if (pollTimerRef.current) { clearInterval(pollTimerRef.current); pollTimerRef.current = null; }
if (msgHandlerRef.current) { window.removeEventListener('message', msgHandlerRef.current); msgHandlerRef.current = null; }
setConnecting(providerId);
trackEvent('onboarding.provider_selected', { provider: providerId });
// Delay before calling connect — avoids Claude OAuth rate limit on retries
await new Promise(r => setTimeout(r, 1000));
@@ -186,6 +197,7 @@ const OnboardingModal: React.FC = () => {
if (pd.success) {
clearInterval(timer);
pollTimerRef.current = null;
trackEvent('onboarding.provider_connected', { provider: providerId });
dismiss();
}
} catch {}
@@ -209,6 +221,7 @@ const OnboardingModal: React.FC = () => {
window.removeEventListener('message', msgHandlerRef.current);
msgHandlerRef.current = null;
}
trackEvent('onboarding.provider_connected', { provider: providerId });
dismiss();
}
} catch {}
@@ -237,6 +250,7 @@ const OnboardingModal: React.FC = () => {
}),
});
} catch {}
trackEvent('onboarding.provider_connected', { provider: providerId });
dismiss();
}
};
@@ -257,8 +271,8 @@ const OnboardingModal: React.FC = () => {
}
};
const handleApiKey = () => dismiss();
const handleSkip = () => dismiss();
const handleApiKey = () => { trackEvent('onboarding.api_key_chosen'); dismiss(); };
const handleSkip = () => { trackEvent(step === 'profile' ? 'onboarding.profile_skipped' : 'onboarding.connect_skipped'); dismiss(); };
if (!open) return null;
@@ -3,6 +3,7 @@ import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { trackEvent } from '@/shared/analytics';
export interface WalkthroughStep {
target: string; // data-onboarding="<value>" selector
@@ -104,6 +105,18 @@ const OnboardingWalkthrough: React.FC<Props> = ({ onComplete }) => {
const totalSteps = STEPS.length;
const isLastStep = currentStep === totalSteps - 1;
// Track walkthrough start on mount
useEffect(() => {
trackEvent('walkthrough.started');
}, []);
// Track each step viewed
useEffect(() => {
if (step) {
trackEvent('walkthrough.step_viewed', { step: currentStep, step_name: step.target || 'done' });
}
}, [currentStep, step]);
// Find target element and compute spotlight + tooltip position
const updatePosition = useCallback(() => {
if (!step) return;
@@ -198,15 +211,17 @@ const OnboardingWalkthrough: React.FC<Props> = ({ onComplete }) => {
const handleNext = useCallback(() => {
if (isLastStep) {
trackEvent('walkthrough.completed', { steps_viewed: currentStep + 1 });
onComplete();
} else {
setCurrentStep((s) => s + 1);
}
}, [isLastStep, onComplete]);
}, [isLastStep, onComplete, currentStep]);
const handleSkip = useCallback(() => {
trackEvent('walkthrough.skipped', { step: currentStep, step_name: step?.target || 'done' });
onComplete();
}, [onComplete]);
}, [onComplete, currentStep, step]);
// Allow clicking the spotlight target to advance for action steps
useEffect(() => {
@@ -219,6 +234,7 @@ const OnboardingWalkthrough: React.FC<Props> = ({ onComplete }) => {
if (!el) return;
const handler = () => {
trackEvent('walkthrough.step_action', { step: currentStep, step_name: step.target });
setTimeout(() => handleNext(), 300);
};
el.addEventListener('click', handler, { once: true });