[eric] add referral question + back button to onboarding, fix flash + jumping

This commit is contained in:
ciregenz
2026-04-07 00:42:12 -07:00
parent 077d9b4011
commit 6db2d24f78
3 changed files with 133 additions and 8 deletions
+1
View File
@@ -52,6 +52,7 @@ class AppSettings(BaseModel):
user_name: Optional[str] = None
user_email: Optional[str] = None
user_use_case: Optional[str] = None
user_referral_source: Optional[str] = None
# Analytics: opted in by default, user can toggle off
analytics_opt_in: bool = True
installation_id: Optional[str] = None
@@ -27,6 +27,20 @@ const USE_CASES = [
'Other',
];
const REFERRAL_SOURCES = [
'Twitter / X',
'LinkedIn',
'YouTube',
'TikTok',
'Reddit',
'Hacker News',
'GitHub',
'Friend / Word of mouth',
'Search engine',
'Blog / Article',
'Other',
];
const OnboardingModal: React.FC = () => {
const c = useClaudeTokens();
const settings = useAppSelector((s) => s.settings);
@@ -35,6 +49,9 @@ const OnboardingModal: React.FC = () => {
const [userName, setUserName] = useState('');
const [userEmail, setUserEmail] = useState('');
const [useCases, setUseCases] = useState<string[]>([]);
const [useCaseOther, setUseCaseOther] = useState<string>('');
const [referralSource, setReferralSource] = useState<string>('');
const [referralSourceOther, setReferralSourceOther] = useState<string>('');
const [connecting, setConnecting] = useState<string | null>(null);
const [nineRouterReady, setNineRouterReady] = useState<boolean | null>(null);
const pollTimerRef = useRef<any>(null);
@@ -134,6 +151,14 @@ const OnboardingModal: React.FC = () => {
try {
const r = await fetch(`${API_BASE}/settings`);
const currentSettings = await r.json();
// If "Other" is selected, replace it with the user's custom text
const resolvedUseCases = useCases.map((u) =>
u === 'Other' && useCaseOther.trim() ? `Other: ${useCaseOther.trim()}` : u
);
const resolvedReferralSource =
referralSource === 'Other' && referralSourceOther.trim()
? `Other: ${referralSourceOther.trim()}`
: referralSource;
await fetch(`${API_BASE}/settings`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
@@ -141,7 +166,8 @@ const OnboardingModal: React.FC = () => {
...currentSettings,
user_name: userName.trim() || null,
user_email: userEmail.trim() || null,
user_use_case: useCases.length > 0 ? useCases.join(', ') : null,
user_use_case: resolvedUseCases.length > 0 ? resolvedUseCases.join(', ') : null,
user_referral_source: resolvedReferralSource || null,
}),
});
} catch {}
@@ -150,6 +176,9 @@ const OnboardingModal: React.FC = () => {
has_email: !!userEmail.trim(),
use_cases: useCases,
use_cases_count: useCases.length,
use_case_other: useCases.includes('Other') ? useCaseOther.trim() : '',
referral_source: referralSource,
referral_source_other: referralSource === 'Other' ? referralSourceOther.trim() : '',
});
setStep('connect');
trackEvent('onboarding.connect_started', { nine_router_ready: nineRouterReady });
@@ -358,6 +387,75 @@ const OnboardingModal: React.FC = () => {
</Box>
))}
</Box>
{useCases.includes('Other') && (
<TextField
placeholder="Tell us what else..."
value={useCaseOther}
onChange={(e) => setUseCaseOther(e.target.value)}
size="small"
fullWidth
sx={{
mt: 0.5,
'& .MuiOutlinedInput-root': {
fontSize: '0.82rem',
color: c.text.primary,
bgcolor: c.bg.input,
borderRadius: `${c.radius.md}px`,
'& fieldset': { borderColor: c.border.subtle },
'&:hover fieldset': { borderColor: c.border.medium },
'&.Mui-focused fieldset': { borderColor: c.accent.primary },
},
'& .MuiOutlinedInput-input::placeholder': { color: c.text.ghost, opacity: 1 },
}}
/>
)}
<Typography sx={{ fontSize: '0.65rem', fontWeight: 600, color: c.text.tertiary, textTransform: 'uppercase', letterSpacing: '0.08em', mt: 0.5 }}>
How did you hear about OpenSwarm?
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.75 }}>
{REFERRAL_SOURCES.map((src) => (
<Box
key={src}
onClick={() => setReferralSource(prev => prev === src ? '' : src)}
sx={{
px: 1.5, py: 0.6,
borderRadius: `${c.radius.md}px`,
border: `1px solid ${referralSource === src ? c.accent.primary : c.border.subtle}`,
bgcolor: referralSource === src ? `${c.accent.primary}15` : 'transparent',
cursor: 'pointer',
transition: 'all 0.15s',
'&:hover': { borderColor: c.border.medium },
}}
>
<Typography sx={{ fontSize: '0.72rem', color: referralSource === src ? c.accent.primary : c.text.secondary }}>
{src}
</Typography>
</Box>
))}
</Box>
{referralSource === 'Other' && (
<TextField
placeholder="Where did you hear about us?"
value={referralSourceOther}
onChange={(e) => setReferralSourceOther(e.target.value)}
size="small"
fullWidth
sx={{
mt: 0.5,
'& .MuiOutlinedInput-root': {
fontSize: '0.82rem',
color: c.text.primary,
bgcolor: c.bg.input,
borderRadius: `${c.radius.md}px`,
'& fieldset': { borderColor: c.border.subtle },
'&:hover fieldset': { borderColor: c.border.medium },
'&.Mui-focused fieldset': { borderColor: c.accent.primary },
},
'& .MuiOutlinedInput-input::placeholder': { color: c.text.ghost, opacity: 1 },
}}
/>
)}
</Box>
<Button
@@ -161,12 +161,13 @@ const OnboardingWalkthrough: React.FC<Props> = ({ onComplete }) => {
const gap = 16;
let tp = { top: 0, left: 0 };
// If target is in the bottom 30% of the screen, place tooltip in the center-upper area
const isBottomTarget = sr.top > window.innerHeight * 0.6;
// If target is in the lower half of the screen, anchor the tooltip at a
// fixed center-upper position so it doesn't shift between toolbar steps.
const isBottomTarget = sr.top > window.innerHeight * 0.5;
if (isBottomTarget) {
tp = {
top: Math.min(sr.top - tooltipH - gap * 2, window.innerHeight * 0.35),
left: window.innerWidth / 2 - tooltipW / 2,
top: Math.round(window.innerHeight * 0.35),
left: Math.round(window.innerWidth / 2 - tooltipW / 2),
};
} else {
switch (step.placement) {
@@ -194,8 +195,11 @@ const OnboardingWalkthrough: React.FC<Props> = ({ onComplete }) => {
}, [step, currentStep, totalSteps]);
useEffect(() => {
setVisible(false);
const timer = setTimeout(updatePosition, 150); // small delay for DOM updates
// Don't toggle visibility between steps — that fades the dark overlay out
// and back in, briefly showing the bright dashboard underneath (the "white
// flash"). Just update positions and let the existing CSS transitions
// smoothly animate the spotlight and tooltip to their new locations.
const timer = setTimeout(updatePosition, 0);
return () => {
clearTimeout(timer);
if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
@@ -218,6 +222,10 @@ const OnboardingWalkthrough: React.FC<Props> = ({ onComplete }) => {
}
}, [isLastStep, onComplete, currentStep]);
const handleBack = useCallback(() => {
setCurrentStep((s) => Math.max(0, s - 1));
}, []);
const handleSkip = useCallback(() => {
trackEvent('walkthrough.skipped', { step: currentStep, step_name: step?.target || 'done' });
onComplete();
@@ -370,7 +378,25 @@ const OnboardingWalkthrough: React.FC<Props> = ({ onComplete }) => {
)}
{/* Buttons */}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Button
onClick={handleBack}
disabled={currentStep === 0}
sx={{
textTransform: 'none',
fontSize: '0.82rem',
fontWeight: 600,
color: c.text.tertiary,
borderRadius: `${c.radius.md}px`,
px: 2,
py: 0.75,
fontFamily: c.font.sans,
visibility: currentStep === 0 || step.target === 'new-agent-button' ? 'hidden' : 'visible',
'&:hover': { bgcolor: c.bg.hover || 'rgba(255,255,255,0.05)' },
}}
>
Back
</Button>
<Button
onClick={handleNext}
sx={{