mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-31 20:29:56 +02:00
[eric] add referral question + back button to onboarding, fix flash + jumping
This commit is contained in:
@@ -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={{
|
||||
|
||||
Reference in New Issue
Block a user