[eric] onboarding polish: exaggerated cursor pop + longer wait then prompt click; faster greeting, no caret; chips slide in smoothly (no instant pop); title 'First chat with OpenSwarm', no draft label

This commit is contained in:
ciregenz
2026-06-14 02:18:53 -07:00
parent 89a1193f32
commit 82907b4601
5 changed files with 24 additions and 20 deletions
@@ -120,13 +120,13 @@ const AgenticCursor = forwardRef<AgenticCursorHandle>((_props, ref) => {
controls.set({ x: from.x, y: from.y, opacity: 0, scale: 0.3 });
setVisible(true);
setBurstKey((k) => k + 1); // replay the orange spark on each pop
// A little "pop!": scale overshoots past 1 then settles, while opacity snaps in.
// An exaggerated "POP!": scale shoots way past 1 then settles back, opacity snaps in.
await controls.start({
opacity: 1,
scale: [0.3, 1.15, 1],
scale: [0.1, 1.45, 0.92, 1],
transition: {
opacity: { duration: 0.16, ease: 'easeOut' },
scale: { duration: 0.36, ease: 'easeOut', times: [0, 0.62, 1] },
opacity: { duration: 0.14, ease: 'easeOut' },
scale: { duration: 0.5, ease: 'easeOut', times: [0, 0.55, 0.8, 1] },
},
});
},
@@ -11,10 +11,10 @@ export const welcomeOpenStep: OnboardingStep = {
title: 'Welcome',
description: '',
ops: [
{ kind: 'delay', ms: 400 }, // brief beat after the pop + spark
{ kind: 'delay', ms: 900 }, // let the big POP land + breathe
{ kind: 'move_to', target: S.newAgentButton },
{ kind: 'popup', text: 'Let me open a chat for you.' },
{ kind: 'delay', ms: 650 }, // a moment to read, then click
{ kind: 'delay', ms: 350 }, // quick read, then click promptly
{ kind: 'click', target: S.newAgentButton, simulate: true }, // spawns the welcome chat
{ kind: 'outro' },
],
@@ -36,9 +36,9 @@ const WelcomeQuickReplies: React.FC<{
onPick: (prompt: string) => void;
onPickBuilder: (prompt: string) => void;
}> = ({ c, onPick, onPickBuilder }) => {
// Slow + delayed so it reads as a calm sequence: card pops, the header title streams, THEN
// the greeting types out unhurried, THEN the chips pop in.
const { shown: greeting, done: greetingDone } = useTypewriter(GREETING, 78, 850);
// Sequence: card pops, the header title streams, THEN the greeting types out, THEN the chips
// slide in. Brisk but smooth.
const { shown: greeting, done: greetingDone } = useTypewriter(GREETING, 42, 450);
const [expanded, setExpanded] = React.useState<string | null>(null);
const currentCategory = STARTER_CATEGORIES.find((cat) => cat.id === expanded);
const isAppBuilder = currentCategory?.target === 'app-builder';
@@ -65,14 +65,17 @@ const WelcomeQuickReplies: React.FC<{
}}
>
{greeting}
{!greetingDone && (
<Box component="span" sx={{ ml: '1px', color: c.accent.primary }}></Box>
)}
</Box>
{/* Chips reveal only once the greeting finishes, each popping in with a stagger. */}
{/* The chips block slides up + fades in once the greeting finishes (the outer motion.div),
then each chip springs in staggered (inner). */}
{greetingDone && (
<Box sx={{ width: '100%', alignSelf: 'stretch' }}>
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: [0.22, 1, 0.36, 1] }}
style={{ width: '100%', alignSelf: 'stretch' }}
>
<AnimatePresence mode="wait" initial={false}>
{expanded === null ? (
<motion.div key="categories" initial={false} style={{ display: 'flex', flexDirection: 'column' }}>
@@ -86,9 +89,9 @@ const WelcomeQuickReplies: React.FC<{
<motion.button
key={cat.id}
onClick={() => setExpanded(cat.id)}
initial={{ opacity: 0, scale: 0.82 }}
initial={{ opacity: 0, scale: 0.85 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ type: 'spring', stiffness: 420, damping: 22, delay: 0.25 + i * 0.18 }}
transition={{ type: 'spring', stiffness: 480, damping: 24, delay: 0.12 + i * 0.08 }}
style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '10px 14px',
@@ -155,7 +158,7 @@ const WelcomeQuickReplies: React.FC<{
</motion.div>
)}
</AnimatePresence>
</Box>
</motion.div>
)}
</Box>
);
@@ -756,8 +756,9 @@ const AgentCard: React.FC<Props> = ({
</Typography>
)}
</Typewriter>
{/* Status speaks only when it needs the user; finished work sits quiet. */}
{session.status !== 'completed' && session.status !== 'stopped' && (
{/* Status speaks only when it needs the user; finished work sits quiet. The welcome
chat hides its 'draft' label so the title reads clean. */}
{session.status !== 'completed' && session.status !== 'stopped' && !session.is_welcome_draft && (
<Box sx={{ display: 'flex', alignItems: 'center', flexShrink: 0 }}>
<Typography sx={{ fontSize: '0.75rem', fontWeight: 500, color: c.text.tertiary, whiteSpace: 'nowrap' }}>
{friendlyStatusLabel(session.status)}
+1 -1
View File
@@ -523,7 +523,7 @@ const agentsSlice = createSlice({
const { draftId, mode, setActive, targetDirectory, model, provider, thinkingLevel, seededMessages, welcome, dashboardId } = action.payload;
state.sessions[draftId] = {
id: draftId,
name: 'New chat',
name: welcome === true ? 'First chat with OpenSwarm' : 'New chat',
status: 'draft',
provider: provider || 'anthropic',
model: model || 'sonnet',