From 55ae134344bc87eb4312afc20ab49dcc2f4b2ce8 Mon Sep 17 00:00:00 2001 From: Tim Black Date: Mon, 4 May 2026 12:03:45 -0700 Subject: [PATCH 1/3] feat: add popular actions carousel for SS-5653 Replace static welcome suggestion buttons with a horizontally scrolling popular actions carousel that auto-advances, pauses during hover/focus/touch interaction, and supports mobile swipe navigation. Co-authored-by: Cursor --- apps/agentic-chat/src/components/Chat.tsx | 24 +--- .../src/components/PopularActionsCarousel.tsx | 134 ++++++++++++++++++ 2 files changed, 139 insertions(+), 19 deletions(-) create mode 100644 apps/agentic-chat/src/components/PopularActionsCarousel.tsx diff --git a/apps/agentic-chat/src/components/Chat.tsx b/apps/agentic-chat/src/components/Chat.tsx index 6de5a268..fa18ee2c 100644 --- a/apps/agentic-chat/src/components/Chat.tsx +++ b/apps/agentic-chat/src/components/Chat.tsx @@ -9,13 +9,15 @@ import { AssistantMessage } from './AssistantMessage' import { AuroraBackground } from './AuroraBackground' import { Composer } from './Composer' import { LoadingIndicator } from './LoadingIndicator' -import { Button } from './ui/Button' +import { PopularActionsCarousel } from './PopularActionsCarousel' import { UserMessage } from './UserMessage' -const WELCOME_SUGGESTIONS = [ +const POPULAR_ACTIONS = [ 'What is my USDC balance on Arbitrum?', 'Swap half my USDC on arb to FOX', 'Give me some info about FOX on Arb', + 'Show my recent transaction activity', + 'Create a stop loss for my ETH position', ] export function Chat() { @@ -117,23 +119,7 @@ export function Chat() { {/* Suggestions above composer - only shown when empty */} - {isEmpty && ( -
-
- {WELCOME_SUGGESTIONS.map((suggestion, index) => ( - - ))} -
-
- )} + {isEmpty && } {/* Composer */}
diff --git a/apps/agentic-chat/src/components/PopularActionsCarousel.tsx b/apps/agentic-chat/src/components/PopularActionsCarousel.tsx new file mode 100644 index 00000000..4ffe82c5 --- /dev/null +++ b/apps/agentic-chat/src/components/PopularActionsCarousel.tsx @@ -0,0 +1,134 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +import { Button } from './ui/Button' + +type PopularActionsCarouselProps = { + actions: string[] + onActionClick: (action: string) => void +} + +const AUTO_ADVANCE_MS = 5000 + +export function PopularActionsCarousel({ actions, onActionClick }: PopularActionsCarouselProps) { + const containerRef = useRef(null) + const [activeIndex, setActiveIndex] = useState(0) + const [isPaused, setIsPaused] = useState(false) + const pauseTimeoutRef = useRef | null>(null) + const rafRef = useRef(null) + + const stopResumeTimer = useCallback(() => { + if (!pauseTimeoutRef.current) return + clearTimeout(pauseTimeoutRef.current) + pauseTimeoutRef.current = null + }, []) + + const resumeAfterInteraction = useCallback(() => { + stopResumeTimer() + pauseTimeoutRef.current = setTimeout(() => { + setIsPaused(false) + pauseTimeoutRef.current = null + }, 1200) + }, [stopResumeTimer]) + + const goToSlide = useCallback((index: number) => { + const container = containerRef.current + if (!container) return + const item = container.querySelector(`[data-action-index="${index}"]`) + if (!item) return + item.scrollIntoView({ behavior: 'smooth', inline: 'start', block: 'nearest' }) + setActiveIndex(index) + }, []) + + const handleScroll = useCallback(() => { + if (rafRef.current) cancelAnimationFrame(rafRef.current) + + rafRef.current = requestAnimationFrame(() => { + const container = containerRef.current + if (!container) return + + const children = Array.from(container.querySelectorAll('[data-action-index]')) + if (children.length === 0) return + + let closestIndex = 0 + let closestDistance = Number.POSITIVE_INFINITY + + children.forEach((child, index) => { + const distance = Math.abs(child.offsetLeft - container.scrollLeft) + if (distance < closestDistance) { + closestDistance = distance + closestIndex = index + } + }) + + setActiveIndex(closestIndex) + }) + }, []) + + useEffect(() => { + if (actions.length <= 1 || isPaused) return + + const timer = setInterval(() => { + const nextIndex = (activeIndex + 1) % actions.length + goToSlide(nextIndex) + }, AUTO_ADVANCE_MS) + + return () => clearInterval(timer) + }, [actions.length, activeIndex, goToSlide, isPaused]) + + useEffect(() => { + return () => { + stopResumeTimer() + if (rafRef.current) cancelAnimationFrame(rafRef.current) + } + }, [stopResumeTimer]) + + return ( +
setIsPaused(true)} + onMouseLeave={() => setIsPaused(false)} + onFocusCapture={() => setIsPaused(true)} + onBlurCapture={() => setIsPaused(false)} + > +
+
{ + stopResumeTimer() + setIsPaused(true) + }} + onTouchEnd={resumeAfterInteraction} + onTouchCancel={resumeAfterInteraction} + role="region" + aria-label="Popular actions" + > + {actions.map((action, index) => ( + + ))} +
+
+ {actions.map((action, index) => ( +
+
+
+ ) +} From 6047d0c5a3c3e217ed04a6e835a448e26d869983 Mon Sep 17 00:00:00 2001 From: Tim Black Date: Thu, 21 May 2026 10:42:02 -0700 Subject: [PATCH 2/3] feat: reorder popular actions for clearer carousel ordering Built with Claude --- apps/agentic-chat/src/components/Chat.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/agentic-chat/src/components/Chat.tsx b/apps/agentic-chat/src/components/Chat.tsx index fa18ee2c..c03e5d00 100644 --- a/apps/agentic-chat/src/components/Chat.tsx +++ b/apps/agentic-chat/src/components/Chat.tsx @@ -13,11 +13,11 @@ import { PopularActionsCarousel } from './PopularActionsCarousel' import { UserMessage } from './UserMessage' const POPULAR_ACTIONS = [ - 'What is my USDC balance on Arbitrum?', - 'Swap half my USDC on arb to FOX', - 'Give me some info about FOX on Arb', 'Show my recent transaction activity', 'Create a stop loss for my ETH position', + 'Swap half my USDC on Ethereum to FOX', + 'What is my USDC balance on Arbitrum?', + 'Give me some info about FOX on Arb', ] export function Chat() { From d3c279a239f63305b65730c420ba03627cee3f2d Mon Sep 17 00:00:00 2001 From: Tim Black Date: Thu, 21 May 2026 11:11:07 -0700 Subject: [PATCH 3/3] fix: aurora background resize and mobile fallback Observe the canvas's parent container and call shader.resize() explicitly so the WebGL aurora re-renders to full size after layout changes (e.g. the sidebar opening/closing). The shader library pins the canvas to fixed pixels and watches the frozen canvas for resizes, so CSS-driven changes never reached it. Also make the mobile CSSFallback more vibrant with layered radial gradients matching the Aurora palette, instead of a faint two-layer wash. Built with Claude --- .../src/components/AuroraBackground.tsx | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/apps/agentic-chat/src/components/AuroraBackground.tsx b/apps/agentic-chat/src/components/AuroraBackground.tsx index ba72985f..f3da6d00 100644 --- a/apps/agentic-chat/src/components/AuroraBackground.tsx +++ b/apps/agentic-chat/src/components/AuroraBackground.tsx @@ -21,12 +21,18 @@ function isWebGLAvailable(): boolean { } function CSSFallback() { + // Approximates the WebGL Aurora's purple-to-green palette (colorA #7B2FBE, + // colorB #00CD98, colorC #A855F7) with layered radial gradients. return (
) @@ -77,7 +83,26 @@ function AuroraCanvas({ onError }: { onError: () => void }) { shader.destroy() return } - cleanup = () => shader.destroy() + + // createShader pins the canvas to a fixed pixel size and watches the + // canvas itself for resizes — so CSS-driven layout changes (e.g. the + // sidebar opening/closing) never reach it. Observe the parent instead + // and resize explicitly. + const parent = canvas.parentElement + let resizeObserver: ResizeObserver | null = null + if (parent) { + resizeObserver = new ResizeObserver(([entry]) => { + if (!entry) return + const { width, height } = entry.contentRect + if (width > 0 && height > 0) shader.resize(width, height) + }) + resizeObserver.observe(parent) + } + + cleanup = () => { + resizeObserver?.disconnect() + shader.destroy() + } }) .catch(err => { console.error('[AuroraBackground] shader init failed, falling back to CSS:', err)