Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ test-output
*.db*
.mastra

# NX cache
.nx/

# Vite cache
node_modules/.vite
**/.vite
Expand All @@ -62,3 +65,7 @@ Thumbs.db

# Playwright MCP
.playwright-mcp

# Nx — local machine cache (agents/CI should never commit these)
.nx/cache
.nx/workspace-data
2 changes: 2 additions & 0 deletions apps/agentic-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"shaders": "^2.5.109",
"sonner": "^2.0.7",
"tailwind-merge": "^3.2.0",
"three": "^0.184.0",
"viem": "*",
"wagmi": "^2.15.6",
"zod": "*",
Expand Down
2 changes: 1 addition & 1 deletion apps/agentic-chat/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const Dashboard = () => {
<SidebarProvider>
{isSidebarLeftEnabled && <SidebarLeft />}
<SidebarInset className="h-dvh flex flex-col">
<header className="sticky top-0 h-12 flex-shrink-0 flex gap-2 bg-background z-10 px-2 items-center">
<header className="sticky top-0 h-12 flex-shrink-0 flex gap-2 bg-background/80 backdrop-blur-md border-b border-border z-10 px-2 items-center">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Gate glass header styles by empty-chat state.

The new glass treatment is always on. Based on the PR objective, this should only apply when the empty state is visible; otherwise the header styling will be inconsistent during active chats.

Suggested direction
-<header className="sticky top-0 h-12 flex-shrink-0 flex gap-2 bg-background/80 backdrop-blur-md border-b border-border z-10 px-2 items-center">
+<header
+  className={cn(
+    "sticky top-0 h-12 flex-shrink-0 flex gap-2 z-10 px-2 items-center",
+    isEmptyChat
+      ? "bg-background/80 backdrop-blur-md border-b border-border"
+      : "bg-background"
+  )}
+>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/agentic-chat/src/app/dashboard/page.tsx` at line 16, The header's glass
styling is always applied; change the JSX in page.tsx so the <header> element
conditionally includes the glass classes ("bg-background/80 backdrop-blur-md")
only when the empty-chat state is true (e.g., use the existing emptyChat /
isEmptyChat boolean or selector used to render the empty state); when not empty,
render the header with the normal non-glass classes (keep border, sizing, and
layout classes intact) so the header appearance matches active chat screens.

<div className="flex items-center gap-2">{isSidebarLeftEnabled && <SidebarTrigger />}</div>
<div className="ml-auto flex items-center gap-2">
<ExportChat />
Expand Down
132 changes: 132 additions & 0 deletions apps/agentic-chat/src/components/AuroraBackground.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { useEffect, useRef, useState } from 'react'

import { useIsMobile } from '@/hooks/use-mobile'

let cachedWebGLAvailable: boolean | null = null
function isWebGLAvailable(): boolean {
if (cachedWebGLAvailable !== null) return cachedWebGLAvailable
try {
const canvas = document.createElement('canvas')
const ctx = window.WebGLRenderingContext && (canvas.getContext('webgl2') ?? canvas.getContext('webgl'))
if (ctx) {
// Release the test context immediately so we don't exhaust the browser's limit
const ext = (ctx as WebGLRenderingContext).getExtension('WEBGL_lose_context')
ext?.loseContext()
}
cachedWebGLAvailable = !!ctx
} catch {
cachedWebGLAvailable = false
}
return cachedWebGLAvailable
}

function CSSFallback() {
// Approximates the WebGL Aurora's purple-to-green palette (colorA #7B2FBE,
// colorB #00CD98, colorC #A855F7) with layered radial gradients.
return (
<div
className="absolute inset-0 w-full h-full"
style={{
background: [
'radial-gradient(ellipse 110% 55% at 50% 0%, rgba(123, 47, 190, 0.55) 0%, rgba(123, 47, 190, 0.18) 38%, transparent 70%)',
'radial-gradient(ellipse 80% 60% at 50% 95%, rgba(0, 205, 152, 0.45) 0%, transparent 72%)',
'radial-gradient(ellipse 65% 50% at 22% 28%, rgba(168, 85, 247, 0.32) 0%, transparent 68%)',
'radial-gradient(ellipse 55% 45% at 82% 62%, rgba(0, 205, 152, 0.22) 0%, transparent 70%)',
].join(', '),
}}
/>
)
}

function AuroraCanvas({ onError }: { onError: () => void }) {
const canvasRef = useRef<HTMLCanvasElement>(null)

useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return

let cleanup: (() => void) | null = null
let cancelled = false

import('shaders/js')
.then(({ createShader }) => {
if (cancelled) return Promise.resolve(null)
return createShader(
canvas,
{
components: [
{
id: 'aurora',
type: 'Aurora',
props: {
colorA: '#7B2FBE',
colorB: '#00CD98',
colorC: '#A855F7',
speed: 3.5,
waviness: 70,
intensity: 90,
curtainCount: 4,
rayDensity: 25,
height: 150,
balance: 40,
colorSpace: 'linear',
},
},
],
},
{ disableTelemetry: true }
)
})
.then(shader => {
if (!shader) return
if (cancelled) {
shader.destroy()
return
}

// 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)
cleanup?.()
cleanup = null
if (!cancelled) onError()
})

return () => {
cancelled = true
cleanup?.()
}
}, [onError])

return <canvas ref={canvasRef} className="absolute inset-0 w-full h-full" style={{ display: 'block' }} />
}

export function AuroraBackground() {
const isMobile = useIsMobile()
const [shaderFailed, setShaderFailed] = useState(false)

if (isMobile || !isWebGLAvailable() || shaderFailed) {
return <CSSFallback />
}

return <AuroraCanvas onError={() => setShaderFailed(true)} />
}
34 changes: 11 additions & 23 deletions apps/agentic-chat/src/components/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import { useStreamPauseDetector } from '../hooks/useStreamPauseDetector'
import { useChatContext } from '../providers/ChatProvider'

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',
]

Expand Down Expand Up @@ -93,8 +96,9 @@ export function Chat() {
{/* Messages viewport */}
<div className="flex-1 overflow-hidden">
{isEmpty ? (
<div className="flex h-full items-center justify-center">
<div className="text-lg text-foreground">How can I help you today?</div>
<div className="relative flex h-full items-center justify-center overflow-hidden">
<AuroraBackground />
<div className="relative z-10 text-lg text-foreground">How can I help you today?</div>
</div>
) : (
<Virtuoso
Expand All @@ -115,26 +119,10 @@ export function Chat() {
</div>

{/* Suggestions above composer - only shown when empty */}
{isEmpty && (
<div className="bg-background">
<div className="mx-auto flex max-w-2xl gap-2 px-4 py-3">
{WELCOME_SUGGESTIONS.map((suggestion, index) => (
<Button
key={index}
onClick={() => handleSuggestionClick(suggestion)}
title={suggestion}
variant="outline"
className="flex-1 min-w-0 h-[52px] line-clamp-2 whitespace-normal"
>
{suggestion}
</Button>
))}
</div>
</div>
)}
{isEmpty && <PopularActionsCarousel actions={POPULAR_ACTIONS} onActionClick={handleSuggestionClick} />}

{/* Composer */}
<div className="bg-background">
<div className={isEmpty ? 'bg-background/80 backdrop-blur-md' : 'bg-background'}>
<div className="mx-auto max-w-2xl p-4">
<Composer />
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/agentic-chat/src/components/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function Composer() {
onKeyDown={onKeyDown}
placeholder="Write a message..."
rows={1}
className="flex-1 resize-none rounded-lg border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
className="flex-1 resize-none rounded-2xl border border-border bg-background px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
style={
{
minHeight: '48px',
Expand Down
134 changes: 134 additions & 0 deletions apps/agentic-chat/src/components/PopularActionsCarousel.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement | null>(null)
const [activeIndex, setActiveIndex] = useState(0)
const [isPaused, setIsPaused] = useState(false)
const pauseTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const rafRef = useRef<number | null>(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<HTMLButtonElement>(`[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<HTMLButtonElement>('[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 (
<div
className="bg-background/80 backdrop-blur-md border-t border-border"
onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
onFocusCapture={() => setIsPaused(true)}
onBlurCapture={() => setIsPaused(false)}
>
<div className="mx-auto max-w-2xl px-4 py-3">
<div
ref={containerRef}
className="flex snap-x snap-mandatory gap-2 overflow-x-auto scroll-smooth pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
onScroll={handleScroll}
onTouchStart={() => {
stopResumeTimer()
setIsPaused(true)
}}
onTouchEnd={resumeAfterInteraction}
onTouchCancel={resumeAfterInteraction}
role="region"
aria-label="Popular actions"
>
{actions.map((action, index) => (
<Button
key={action}
data-action-index={index}
onClick={() => onActionClick(action)}
title={action}
variant="outline"
className="h-[52px] w-[85%] shrink-0 snap-start whitespace-normal text-left leading-tight sm:w-[calc(50%-0.25rem)]"
>
{action}
</Button>
))}
</div>
<div className="mt-2 flex justify-center gap-1.5">
{actions.map((action, index) => (
<button
key={`dot-${action}`}
type="button"
onClick={() => goToSlide(index)}
aria-label={`Show action ${index + 1}`}
className={`h-1.5 w-1.5 rounded-full transition-colors ${index === activeIndex ? 'bg-foreground' : 'bg-muted-foreground/30'}`}
/>
))}
</div>
</div>
</div>
)
}
2 changes: 1 addition & 1 deletion apps/agentic-chat/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
}

:root {
--radius: 0.5rem;
--radius: 0.75rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
Expand Down
3 changes: 3 additions & 0 deletions apps/agentic-chat/vite.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig(() => ({
plugins: [react(), tailwindcss()],
optimizeDeps: {
include: ['three'],
},
envDir: resolve(__dirname, '../..'),
cacheDir: resolve(__dirname, '../../node_modules/.vite/agentic-chat'),
define: {
Expand Down
Loading
Loading