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) diff --git a/apps/agentic-chat/src/components/Chat.tsx b/apps/agentic-chat/src/components/Chat.tsx index 6de5a268..c03e5d00 100644 --- a/apps/agentic-chat/src/components/Chat.tsx +++ b/apps/agentic-chat/src/components/Chat.tsx @@ -9,12 +9,14 @@ 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 = [ + '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?', - 'Swap half my USDC on arb to FOX', 'Give me some info about FOX on Arb', ] @@ -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) => ( +
+
+
+ ) +}