mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-10-01 05:54:56 +02:00
[eric] onboarding flow, user ui/ux
This commit is contained in:
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user