From ba850e5fdc094e9483566c2d1b1b7fc3b94697bf Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sun, 14 Jun 2026 01:43:13 -0700 Subject: [PATCH] [eric] onboarding: slower sequential welcome reveal (title -> greeting -> chips) + cursor bursts in with radiating orange spark lines --- .../Onboarding/ac/AgenticCursor.tsx | 61 ++++++++++++++----- .../pages/AgentChat/WelcomeQuickReplies.tsx | 30 +++++---- 2 files changed, 63 insertions(+), 28 deletions(-) diff --git a/frontend/src/app/components/Onboarding/ac/AgenticCursor.tsx b/frontend/src/app/components/Onboarding/ac/AgenticCursor.tsx index 796097c3..85edbcf0 100644 --- a/frontend/src/app/components/Onboarding/ac/AgenticCursor.tsx +++ b/frontend/src/app/components/Onboarding/ac/AgenticCursor.tsx @@ -62,12 +62,34 @@ const IS_WIN = typeof navigator !== 'undefined' && navigator.userAgent.includes( // before the next step's instant write lands. const WIN_EASE_MS = 420; +// A little spark when the cursor pops into existence: short orange lines shoot out from the +// tip and fade. Re-keyed on each pop so it replays. One-shot per mount (no infinite loop). +const BURST_SPOKES = 8; +const CursorBurst: React.FC<{ color: string }> = ({ color }) => ( + <> + {Array.from({ length: BURST_SPOKES }).map((_, i) => ( +
+ +
+ ))} + +); + const AgenticCursor = forwardRef((_props, ref) => { const c = useClaudeTokens(); const controls = useAnimationControls(); const storePos = useCursorPosition(); const posRef = useRef({ x: 0, y: 0 }); const [visible, setVisible] = useState(false); + const [burstKey, setBurstKey] = useState(0); const [popup, setPopup] = useState(null); const [multiChoice, setMultiChoice] = useState(null); @@ -97,6 +119,7 @@ const AgenticCursor = forwardRef((_props, ref) => { writePos(from.x, from.y, true); 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. await controls.start({ opacity: 1, @@ -308,22 +331,28 @@ const AgenticCursor = forwardRef((_props, ref) => { }} > {visible && ( - - - + <> + {/* Orange spark behind the arrow, replays each pop via burstKey. */} +
+ +
+ + + + )} diff --git a/frontend/src/app/pages/AgentChat/WelcomeQuickReplies.tsx b/frontend/src/app/pages/AgentChat/WelcomeQuickReplies.tsx index 0e3795f3..d6a917b4 100644 --- a/frontend/src/app/pages/AgentChat/WelcomeQuickReplies.tsx +++ b/frontend/src/app/pages/AgentChat/WelcomeQuickReplies.tsx @@ -8,19 +8,23 @@ import { STARTER_CATEGORIES } from '@/shared/starterCategories'; const GREETING = "Hi, I'm your AI team. What do you want done?"; -// One-shot typewriter for a fixed string (no infinite loop; stops at the end). -function useTypewriter(text: string, speedMs = 26): { shown: string; done: boolean } { +// One-shot typewriter for a fixed string (no infinite loop; stops at the end). startDelayMs +// holds the start so the header title can stream first (sequential reveal). +function useTypewriter(text: string, speedMs = 45, startDelayMs = 0): { shown: string; done: boolean } { const [shown, setShown] = React.useState(''); React.useEffect(() => { setShown(''); - let i = 0; - const id = window.setInterval(() => { - i += 1; - setShown(text.slice(0, i)); - if (i >= text.length) window.clearInterval(id); - }, speedMs); - return () => window.clearInterval(id); - }, [text, speedMs]); + let interval: number | undefined; + const startTimer = window.setTimeout(() => { + let i = 0; + interval = window.setInterval(() => { + i += 1; + setShown(text.slice(0, i)); + if (i >= text.length) window.clearInterval(interval); + }, speedMs); + }, startDelayMs); + return () => { window.clearTimeout(startTimer); if (interval) window.clearInterval(interval); }; + }, [text, speedMs, startDelayMs]); return { shown, done: shown.length >= text.length }; } @@ -32,7 +36,9 @@ const WelcomeQuickReplies: React.FC<{ onPick: (prompt: string) => void; onPickBuilder: (prompt: string) => void; }> = ({ c, onPick, onPickBuilder }) => { - const { shown: greeting, done: greetingDone } = useTypewriter(GREETING); + // Slow + delayed so it reads as a sequence: card pops, the header title streams, THEN + // the greeting types, THEN the chips pop in. + const { shown: greeting, done: greetingDone } = useTypewriter(GREETING, 46, 650); const [expanded, setExpanded] = React.useState(null); const currentCategory = STARTER_CATEGORIES.find((cat) => cat.id === expanded); const isAppBuilder = currentCategory?.target === 'app-builder'; @@ -82,7 +88,7 @@ const WelcomeQuickReplies: React.FC<{ onClick={() => setExpanded(cat.id)} initial={{ opacity: 0, scale: 0.82 }} animate={{ opacity: 1, scale: 1 }} - transition={{ type: 'spring', stiffness: 520, damping: 24, delay: 0.08 + i * 0.07 }} + transition={{ type: 'spring', stiffness: 420, damping: 22, delay: 0.18 + i * 0.14 }} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 14px',