[eric] onboarding polish: 5-step pre-connect walkthrough slideshow, enable Gemini/ChatGPT connect in the modal, drop Copilot from Settings, fix modal flash-dismiss for

returning Pro users, remove Try-your-first-task card
This commit is contained in:
ciregenz
2026-04-16 11:55:08 -07:00
parent 7eb85a7127
commit 4fb63ad688
4 changed files with 192 additions and 343 deletions
+185 -9
View File
@@ -51,6 +51,44 @@ const SUBSCRIPTION_PROVIDERS = [
{ id: 'codex', name: 'ChatGPT', desc: 'GPT-5.4, GPT-5.4 Mini, GPT-5.3 Codex', color: '#74AA9C', preview: false },
];
const EDUCATION_STEPS: { title: string; body: string[] }[] = [
{
title: 'Launch an Agent',
body: [
'The core functionality of OpenSwarm revolves around Agents. Click the "+" button in your toolbar to launch a new Agent. Agents can take actions for you, work with files on your computer, and much more.',
'Within an Agent\u2019s input, you can choose what AI model powers it, the mode it runs in, and attach context via "@" and "/" commands.',
],
},
{
title: 'Connect your actions',
body: [
'Actions are how your Agents interact with the outside world \u2014 Gmail, Google Calendar, Notion, Slack, and more. Head to the Connections page to link your accounts and unlock what your Agents can do.',
'Once connected, your Agents can read emails, create events, update databases, and take real actions across your tools \u2014 all from a single conversation.',
],
},
{
title: 'Browsers',
body: [
'OpenSwarm has built-in browsers so you never have to jump between apps. Stay in one place, stay in the zone \u2014 just one seamless workspace for you and your Agents.',
"And when you'd rather not do it yourself, let an Agent open & control browsers, navigate sites, fill out forms, and complete tasks end-to-end while you watch it happen in real time.",
],
},
{
title: 'Select and send',
body: [
"Select any existing browser or agent in your dashboard to let a new agent control it. It's the fastest way to get things done \u2014 just select something on your dashboard and let your Agent handle the rest.",
'No copy-pasting, no context-switching. Just select, send, and let your Agent take it from there.',
],
},
{
title: 'Make it yours',
body: [
'Customize how OpenSwarm works with Skills, Modes, and Apps. Skills teach your Agents reusable workflows. Modes let you switch between different behavior profiles. Apps are standalone applications that run directly in OpenSwarm that Agents build for you.',
'Explore the Customization & Apps sections in the sidebar to get started \u2014 or just ask an Agent to help you create your first Skill.',
],
},
];
const USE_CASES = [
'Software Development',
'Research & Analysis',
@@ -84,7 +122,8 @@ const OnboardingModal: React.FC = () => {
const c = useClaudeTokens();
const settings = useAppSelector((s) => s.settings);
const [open, setOpen] = useState(false);
const [step, setStep] = useState<'profile' | 'connect'>('profile');
const [step, setStep] = useState<'profile' | 'walkthrough' | 'connect'>('profile');
const [walkthroughIdx, setWalkthroughIdx] = useState(0);
const [userName, setUserName] = useState('');
const [userEmail, setUserEmail] = useState('');
const [emailBlurred, setEmailBlurred] = useState(false);
@@ -242,8 +281,26 @@ const OnboardingModal: React.FC = () => {
referral_source: referralSource,
referral_source_other: referralSource === 'Other' ? referralSourceOther.trim() : '',
});
setStep('connect');
trackEvent('onboarding.connect_started', { nine_router_ready: nineRouterReady });
setStep('walkthrough');
setWalkthroughIdx(0);
trackEvent('onboarding.education_started');
};
const advanceWalkthrough = () => {
const next = walkthroughIdx + 1;
const currentTitle = EDUCATION_STEPS[walkthroughIdx]?.title;
if (next >= EDUCATION_STEPS.length) {
trackEvent('onboarding.education_completed');
setStep('connect');
trackEvent('onboarding.connect_started', { nine_router_ready: nineRouterReady });
return;
}
trackEvent('onboarding.education_step_advanced', { from: walkthroughIdx, title: currentTitle });
setWalkthroughIdx(next);
};
const backWalkthrough = () => {
setWalkthroughIdx((i) => Math.max(0, i - 1));
};
// Whether all required profile fields are filled in.
@@ -416,20 +473,28 @@ const OnboardingModal: React.FC = () => {
};
const handleApiKey = () => { trackEvent('onboarding.api_key_chosen'); dismiss(); };
const handleSkip = () => { trackEvent(step === 'profile' ? 'onboarding.profile_skipped' : 'onboarding.connect_skipped'); dismiss(); };
const handleSkip = () => {
trackEvent(step === 'profile' ? 'onboarding.profile_skipped' : 'onboarding.connect_skipped');
dismiss();
};
if (!open) return null;
return (
<Modal open={open} onClose={step === 'connect' ? handleSkip : undefined} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Box sx={{
width: 480, maxWidth: '90vw', bgcolor: c.bg.surface, borderRadius: `${c.radius.xl}px`,
border: `1px solid ${c.border.subtle}`, p: 3.5, outline: 'none',
width: step === 'walkthrough' ? 600 : 480, maxWidth: '90vw',
bgcolor: c.bg.surface, borderRadius: `${c.radius.xl}px`,
border: `1px solid ${c.border.subtle}`,
p: step === 'walkthrough' ? 0 : 3.5, outline: 'none',
overflow: 'hidden',
boxShadow: '0 20px 60px rgba(0,0,0,0.4)',
}}>
<Typography sx={{ fontSize: '1.3rem', fontWeight: 700, color: c.text.primary, mb: 0.5, textAlign: 'center' }}>
Welcome to OpenSwarm
</Typography>
{step !== 'walkthrough' && (
<Typography sx={{ fontSize: '1.3rem', fontWeight: 700, color: c.text.primary, mb: 0.5, textAlign: 'center' }}>
Welcome to OpenSwarm
</Typography>
)}
{step === 'profile' ? (
<>
@@ -627,6 +692,117 @@ const OnboardingModal: React.FC = () => {
Continue
</Button>
</>
) : step === 'walkthrough' ? (
<>
{/* Hero image area — soft pastel multi-color blob. Proportionally
larger than the content area so it reads as the visual anchor. */}
<Box
sx={{
position: 'relative',
width: '100%',
height: 400,
background: `
radial-gradient(circle at 18% 78%, #F5A574 0%, rgba(245,165,116,0) 48%),
radial-gradient(circle at 58% 55%, #E9A5D0 0%, rgba(233,165,208,0) 52%),
radial-gradient(circle at 82% 22%, #B9C9F4 0%, rgba(185,201,244,0) 58%),
linear-gradient(135deg, #C4D0F2 0%, #EDB3CC 50%, #F5B088 100%)
`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
}}
>
<Typography
sx={{
position: 'relative',
zIndex: 2,
fontSize: 88,
color: '#fff',
filter: 'drop-shadow(0 4px 16px rgba(0,0,0,0.18))',
lineHeight: 1,
}}
>
</Typography>
</Box>
<Box sx={{ px: 3, pt: 2, pb: 2.5 }}>
<Box sx={{ display: 'flex', gap: 0.5, justifyContent: 'center', mb: 1.75 }}>
{EDUCATION_STEPS.map((_, i) => (
<Box
key={i}
sx={{
width: i === walkthroughIdx ? 16 : 5,
height: 5,
borderRadius: 3,
bgcolor: i === walkthroughIdx ? c.accent.primary : i < walkthroughIdx ? c.accent.primary + '60' : c.border.medium,
transition: 'all 0.25s',
}}
/>
))}
</Box>
<Box
sx={{
display: 'inline-block',
fontSize: '0.62rem',
fontWeight: 700,
color: c.accent.primary,
bgcolor: c.accent.primary + '1a',
border: `1px solid ${c.accent.primary}33`,
letterSpacing: '0.12em',
textTransform: 'uppercase',
px: 1.1,
py: 0.35,
borderRadius: `${c.radius.sm}px`,
mb: 1,
fontFamily: c.font.sans,
}}
>
Step {walkthroughIdx + 1}
</Box>
<Typography sx={{ fontSize: '1.1rem', fontWeight: 700, color: c.text.primary, mb: 1 }}>
{EDUCATION_STEPS[walkthroughIdx].title}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 2.5, minHeight: 130 }}>
{EDUCATION_STEPS[walkthroughIdx].body.map((p, i) => (
<Typography key={i} sx={{ fontSize: '0.92rem', color: c.text.secondary, lineHeight: 1.6 }}>
{p}
</Typography>
))}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Button
onClick={backWalkthrough}
disabled={walkthroughIdx === 0}
sx={{
textTransform: 'none', fontSize: '0.82rem', fontWeight: 600,
color: c.text.tertiary, borderRadius: `${c.radius.md}px`, px: 1.5, py: 0.75,
visibility: walkthroughIdx === 0 ? 'hidden' : 'visible',
'&:hover': { bgcolor: `${c.accent.primary}08` },
}}
>
← Back
</Button>
<Box sx={{ flex: 1 }} />
<Button
onClick={advanceWalkthrough}
sx={{
textTransform: 'none', fontSize: '0.82rem', fontWeight: 600,
bgcolor: c.accent.primary, color: '#fff',
borderRadius: `${c.radius.md}px`, px: 2.25, py: 0.75,
'&:hover': { bgcolor: c.accent.hover },
}}
>
{walkthroughIdx === EDUCATION_STEPS.length - 1 ? 'Continue' : 'Next'}
</Button>
</Box>
</Box>
</>
) : (
<>
<Typography sx={{ fontSize: '0.78rem', color: c.text.muted, mb: 3, textAlign: 'center' }}>
@@ -12,7 +12,6 @@ export interface WalkthroughStep {
placement: 'top' | 'bottom' | 'left' | 'right';
actionHint?: string; // e.g. "Click the + button"
waitForTarget?: boolean; // pause until target appears in DOM
centerOverlay?: boolean; // no spotlight, show card in center
}
const STEPS: WalkthroughStep[] = [
@@ -66,31 +65,6 @@ const STEPS: WalkthroughStep[] = [
description: 'Create simple apps powered by AI \u2014 dashboards, forms, data tools. Just describe what you want.',
placement: 'right',
},
{
target: '',
title: 'Try your first task',
description: "OpenSwarm is most useful when you give it something real. Open a new chat and try one of these:",
placement: 'bottom',
centerOverlay: true,
},
];
const EXAMPLE_PROMPTS: { emoji: string; label: string; prompt: string }[] = [
{
emoji: '🔎',
label: 'Research a topic',
prompt: 'Research the latest developments in AI agents and give me a short briefing',
},
{
emoji: '🛠️',
label: 'Build a mini app',
prompt: 'Build me a simple habit tracker with a clean UI',
},
{
emoji: '📄',
label: 'Summarize a webpage',
prompt: 'Browse https://news.ycombinator.com and summarize the top 5 stories',
},
];
interface Props {
@@ -133,19 +107,6 @@ const OnboardingWalkthrough: React.FC<Props> = ({ onComplete }) => {
const updatePosition = useCallback(() => {
if (!step) return;
if (step.centerOverlay) {
setSpotlightRect(null);
const isAnnouncement = currentStep === STEPS.length - 1;
const w = isAnnouncement ? 600 : 320;
const h = isAnnouncement ? 640 : 200;
setTooltipPos({
top: Math.max(24, window.innerHeight / 2 - h / 2),
left: Math.max(24, window.innerWidth / 2 - w / 2),
});
setVisible(true);
return;
}
const el = (
document.querySelector(`[data-onboarding="${step.target}"]`) ||
document.querySelector(`[data-select-type="${step.target}"]`)
@@ -244,34 +205,9 @@ const OnboardingWalkthrough: React.FC<Props> = ({ onComplete }) => {
setCurrentStep((s) => Math.max(0, s - 1));
}, []);
const handleSkip = useCallback(() => {
trackEvent('walkthrough.skipped', { step: currentStep, step_name: step?.target || 'done' });
onComplete();
}, [onComplete, currentStep, step]);
const handleStartFirstChat = useCallback((example?: { label: string; prompt: string }) => {
trackEvent('walkthrough.first_chat_started', { example: example?.label || 'cta' });
if (example?.prompt) {
try {
sessionStorage.setItem('openswarm_first_prompt', example.prompt);
} catch {}
} else {
try {
sessionStorage.removeItem('openswarm_first_prompt');
} catch {}
}
onComplete();
setTimeout(() => {
const btn = document.querySelector(
'[data-onboarding="new-agent-button"]',
) as HTMLElement | null;
btn?.click();
}, 150);
}, [onComplete]);
// Allow clicking the spotlight target to advance for action steps
useEffect(() => {
if (!step?.actionHint || step.centerOverlay) return;
if (!step?.actionHint) return;
const el = (
document.querySelector(`[data-onboarding="${step.target}"]`) ||
@@ -349,12 +285,12 @@ const OnboardingWalkthrough: React.FC<Props> = ({ onComplete }) => {
position: 'absolute',
top: tooltipPos.top,
left: tooltipPos.left,
width: isLastStep ? 600 : 320,
width: 320,
bgcolor: c.bg.surface,
border: `1px solid ${c.border.medium}`,
borderRadius: `${c.radius.xl}px`,
boxShadow: '0 16px 48px rgba(0,0,0,0.35)',
p: isLastStep ? 0 : 2.5,
p: 2.5,
overflow: 'hidden',
transition: 'top 0.4s cubic-bezier(0.4, 0, 0.2, 1), left 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s',
opacity: visible ? 1 : 0,
@@ -362,175 +298,6 @@ const OnboardingWalkthrough: React.FC<Props> = ({ onComplete }) => {
zIndex: 10000,
}}
>
{isLastStep ? (
<>
{/* Hero image area — soft pastel multi-color blob */}
<Box
sx={{
position: 'relative',
width: '100%',
height: 320,
background: `
radial-gradient(circle at 18% 78%, #F5A574 0%, rgba(245,165,116,0) 48%),
radial-gradient(circle at 58% 55%, #E9A5D0 0%, rgba(233,165,208,0) 52%),
radial-gradient(circle at 82% 22%, #B9C9F4 0%, rgba(185,201,244,0) 58%),
linear-gradient(135deg, #C4D0F2 0%, #EDB3CC 50%, #F5B088 100%)
`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
}}
>
<Typography
sx={{
position: 'relative',
zIndex: 2,
fontSize: 72,
color: '#fff',
filter: 'drop-shadow(0 4px 16px rgba(0,0,0,0.18))',
lineHeight: 1,
}}
>
</Typography>
</Box>
<Box sx={{ p: 2.25 }}>
{/* Step counter dots */}
<Box sx={{ display: 'flex', gap: 0.5, mb: 1.25, justifyContent: 'center' }}>
{STEPS.map((_, i) => (
<Box
key={i}
sx={{
width: i === currentStep ? 14 : 4,
height: 4,
borderRadius: 3,
bgcolor: i === currentStep ? c.accent.primary : i < currentStep ? c.accent.primary + '60' : c.border.medium,
transition: 'all 0.3s',
}}
/>
))}
</Box>
<Box
sx={{
display: 'inline-block',
fontSize: '0.58rem',
fontWeight: 700,
color: c.accent.primary,
bgcolor: c.accent.primary + '1f',
px: 0.85,
py: 0.2,
borderRadius: `${c.radius.xs}px`,
letterSpacing: '0.5px',
mb: 0.75,
fontFamily: c.font.sans,
}}
>
READY TO TRY
</Box>
<Typography
sx={{
fontSize: '0.92rem',
fontWeight: 700,
color: c.text.primary,
mb: 0.35,
fontFamily: c.font.sans,
}}
>
{step.title}
</Typography>
<Typography
sx={{
fontSize: '0.72rem',
color: c.text.secondary,
lineHeight: 1.45,
mb: 1.25,
fontFamily: c.font.sans,
}}
>
{step.description}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, mb: 1.5 }}>
{EXAMPLE_PROMPTS.map((ex) => (
<Box
key={ex.label}
onClick={() => handleStartFirstChat(ex)}
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
px: 1,
py: 0.75,
borderRadius: `${c.radius.md}px`,
border: `1px solid ${c.border.subtle}`,
bgcolor: c.bg.elevated || c.bg.surface,
cursor: 'pointer',
transition: 'all 0.15s',
'&:hover': {
borderColor: c.accent.primary,
bgcolor: c.accent.primary + '10',
transform: 'translateX(2px)',
},
}}
>
<Box sx={{ fontSize: '0.95rem', lineHeight: 1 }}>{ex.emoji}</Box>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography sx={{ fontSize: '0.72rem', fontWeight: 600, color: c.text.primary, fontFamily: c.font.sans }}>
{ex.label}
</Typography>
<Typography sx={{ fontSize: '0.64rem', color: c.text.tertiary, fontFamily: c.font.sans, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{ex.prompt}
</Typography>
</Box>
<Typography sx={{ fontSize: '0.8rem', color: c.text.tertiary, fontFamily: c.font.sans }}></Typography>
</Box>
))}
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Button
onClick={handleSkip}
sx={{
textTransform: 'none',
fontSize: '0.72rem',
fontWeight: 600,
color: c.text.tertiary,
borderRadius: `${c.radius.md}px`,
px: 1.25,
py: 0.5,
fontFamily: c.font.sans,
'&:hover': { bgcolor: 'rgba(255,255,255,0.05)' },
}}
>
Not now
</Button>
<Button
onClick={() => handleStartFirstChat()}
sx={{
textTransform: 'none',
fontSize: '0.72rem',
fontWeight: 600,
bgcolor: c.accent.primary,
color: '#fff',
borderRadius: `${c.radius.md}px`,
px: 2,
py: 0.5,
fontFamily: c.font.sans,
'&:hover': { bgcolor: c.accent.hover || c.accent.primary },
}}
>
Start a new chat
</Button>
</Box>
</Box>
</>
) : (
<>
{/* Step counter dots */}
<Box sx={{ display: 'flex', gap: 0.5, mb: 1.5, justifyContent: 'center' }}>
{STEPS.map((_, i) => (
@@ -623,8 +390,6 @@ const OnboardingWalkthrough: React.FC<Props> = ({ onComplete }) => {
{isLastStep ? 'Get Started' : 'Next'}
</Button>
</Box>
</>
)}
</Box>
</Box>
);
+3 -95
View File
@@ -1,4 +1,4 @@
import React, { useState, useRef, useCallback, useEffect, useLayoutEffect, useMemo, forwardRef, useImperativeHandle } from 'react';
import React, { useState, useRef, useCallback, useEffect, useMemo, forwardRef, useImperativeHandle } from 'react';
import Box from '@mui/material/Box';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
@@ -192,43 +192,6 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
}, []);
const [hasContent, setHasContent] = useState(() => !!_draftStore.get(ownerId));
// Ghost prompt suggestion from onboarding — typed out animated, click or press Enter to run.
const [ghostPrompt, setGhostPrompt] = useState<string | null>(null);
const [ghostTyped, setGhostTyped] = useState(0);
const ghostOverlayRef = useRef<HTMLDivElement>(null);
const [ghostHeight, setGhostHeight] = useState(0);
useEffect(() => {
let pending: string | null = null;
try {
pending = sessionStorage.getItem('openswarm_first_prompt');
if (pending) sessionStorage.removeItem('openswarm_first_prompt');
} catch {}
if (!pending) return;
if (_draftStore.get(ownerId)) return; // user already has a draft; don't overwrite
setGhostPrompt(pending);
setGhostTyped(0);
let i = 0;
const id = window.setInterval(() => {
i += 1;
setGhostTyped(i);
if (i >= pending!.length) window.clearInterval(id);
}, 29);
return () => window.clearInterval(id);
// Only on mount; ownerId is stable.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Measure the ghost overlay so the editor can reserve matching height and
// the toolbar row below doesn't get overlapped when the ghost text wraps.
useLayoutEffect(() => {
if (!ghostPrompt || hasContent) { setGhostHeight(0); return; }
const el = ghostOverlayRef.current;
if (!el) return;
const h = el.offsetHeight;
setGhostHeight((prev) => (prev === h ? prev : h));
}, [ghostPrompt, ghostTyped, hasContent]);
const [attachedSkills, setAttachedSkills] = useState<Record<string, AttachedSkill>>({});
const attachedSkillsRef = useRef(attachedSkills);
attachedSkillsRef.current = attachedSkills;
@@ -508,25 +471,6 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
elementSelection?.clearOwnerElements(ownerId);
}, [disabled, images, contextPaths, forcedTools, onSend, elementSelection, ownerId]);
const acceptGhostPrompt = useCallback(() => {
const editor = editorRef.current;
if (!editor || !ghostPrompt) return;
editor.textContent = ghostPrompt;
setGhostPrompt(null);
setHasContent(true);
_draftStore.set(ownerId, editor.innerHTML);
setTimeout(() => {
editor.focus();
const range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false);
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
handleSend();
}, 0);
}, [ghostPrompt, ownerId, handleSend]);
const detectTrigger = useCallback(() => {
const result = detectEditorTrigger();
if (result) {
@@ -631,10 +575,6 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
}
if (e.key === 'Enter' && !e.shiftKey && !autoRunMode) {
e.preventDefault();
if (ghostPrompt && !hasContent) {
acceptGhostPrompt();
return;
}
handleSend();
}
};
@@ -1026,7 +966,7 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
onPaste={handlePaste}
style={{
width: '100%',
minHeight: ghostPrompt && !hasContent && ghostHeight > 0 ? `${ghostHeight}px` : '1.5em',
minHeight: '1.5em',
maxHeight: 200,
overflowY: 'auto',
background: 'transparent',
@@ -1040,7 +980,7 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
whiteSpace: 'pre-wrap',
}}
/>
{!hasContent && !ghostPrompt && (
{!hasContent && (
<div
style={{
position: 'absolute',
@@ -1059,38 +999,6 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
{disabled ? 'Agent is working...' : autoRunMode ? 'Describe what data to generate…' : isRunning ? (queueLength > 0 ? `${queueLength} queued — type another or wait…` : 'Agent is working — messages will queue…') : `${modeConf.label}, @ for context, / for commands`}
</div>
)}
{!hasContent && ghostPrompt && (
<div
ref={ghostOverlayRef}
onClick={acceptGhostPrompt}
title="Press Enter or click to run"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
padding: `${hasAttachments ? 4 : 10}px 12px`,
color: c.text.secondary,
opacity: 0.55,
fontSize: '0.875rem',
lineHeight: '1.5',
fontFamily: 'inherit',
cursor: 'pointer',
userSelect: 'none',
}}
>
{ghostPrompt.slice(0, ghostTyped)}
{ghostTyped < ghostPrompt.length && (
<span style={{ opacity: 0.8, animation: 'openswarm-ghost-caret 0.9s steps(1) infinite' }}></span>
)}
{ghostTyped >= ghostPrompt.length && (
<span style={{ marginLeft: 8, fontSize: '0.7rem', color: c.text.tertiary, opacity: 0.9 }}>
to run
</span>
)}
<style>{`@keyframes openswarm-ghost-caret { 50% { opacity: 0; } }`}</style>
</div>
)}
</Box>
<Box
File diff suppressed because one or more lines are too long