From 1372dbefb27ca7ffb084b74f9fc88df74f9956e9 Mon Sep 17 00:00:00 2001 From: Henry Li Date: Wed, 21 Jan 2026 10:31:54 +0800 Subject: [PATCH] feat: bring back the deer --- frontend/components.json | 3 +- frontend/public/images/deer.svg | 6 + .../workspace/chats/[thread_id]/layout.tsx | 37 +++- .../app/workspace/chats/[thread_id]/page.tsx | 13 +- .../src/components/ui/flickering-grid.tsx | 194 ++++++++++++++++++ .../src/components/workspace/input-box.tsx | 24 ++- 6 files changed, 266 insertions(+), 11 deletions(-) create mode 100644 frontend/public/images/deer.svg create mode 100644 frontend/src/components/ui/flickering-grid.tsx diff --git a/frontend/components.json b/frontend/components.json index 7996635..63b833f 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -19,6 +19,7 @@ "hooks": "@/hooks" }, "registries": { - "@ai-elements": "https://registry.ai-sdk.dev/{name}.json" + "@ai-elements": "https://registry.ai-sdk.dev/{name}.json", + "@magicui": "https://magicui.design/r/{name}" } } diff --git a/frontend/public/images/deer.svg b/frontend/public/images/deer.svg new file mode 100644 index 0000000..9bbfb29 --- /dev/null +++ b/frontend/public/images/deer.svg @@ -0,0 +1,6 @@ + + + + diff --git a/frontend/src/app/workspace/chats/[thread_id]/layout.tsx b/frontend/src/app/workspace/chats/[thread_id]/layout.tsx index 744493e..2013e58 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/layout.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/layout.tsx @@ -1,5 +1,10 @@ "use client"; +import { usePathname } from "next/navigation"; +import { useTheme } from "next-themes"; +import { useMemo } from "react"; + +import { FlickeringGrid } from "@/components/ui/flickering-grid"; import { ArtifactsProvider } from "@/components/workspace/artifacts"; export default function ChatLayout({ @@ -7,5 +12,35 @@ export default function ChatLayout({ }: { children: React.ReactNode; }) { - return {children}; + const pathname = usePathname(); + const isNewThread = useMemo(() => { + return pathname === "/workspace/chats/new"; + }, [pathname]); + const { theme, systemTheme } = useTheme(); + const currentTheme = theme === "system" ? systemTheme : theme; + return ( + + {isNewThread && ( + <> + + + + )} + {children} + + ); } diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index 4af06a3..9bb6447 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -101,7 +101,14 @@ export default function ChatPage() { minSize={artifactsOpen ? 30 : 100} >
-
+
{title !== "Untitled" && ( @@ -144,14 +151,14 @@ export default function ChatPage() { >
{ + squareSize?: number + gridGap?: number + flickerChance?: number + color?: string + width?: number + height?: number + className?: string + maxOpacity?: number +} + +export const FlickeringGrid: React.FC = ({ + squareSize = 4, + gridGap = 6, + flickerChance = 0.3, + color = "rgb(0, 0, 0)", + width, + height, + className, + maxOpacity = 0.3, + ...props +}) => { + const canvasRef = useRef(null) + const containerRef = useRef(null) + const [isInView, setIsInView] = useState(false) + const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 }) + + const memoizedColor = useMemo(() => { + const toRGBA = (color: string) => { + if (typeof window === "undefined") { + return `rgba(0, 0, 0,` + } + const canvas = document.createElement("canvas") + canvas.width = canvas.height = 1 + const ctx = canvas.getContext("2d") + if (!ctx) return "rgba(255, 0, 0," + ctx.fillStyle = color + ctx.fillRect(0, 0, 1, 1) + const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data) + return `rgba(${r}, ${g}, ${b},` + } + return toRGBA(color) + }, [color]) + + const setupCanvas = useCallback( + (canvas: HTMLCanvasElement, width: number, height: number) => { + const dpr = window.devicePixelRatio || 1 + canvas.width = width * dpr + canvas.height = height * dpr + canvas.style.width = `${width}px` + canvas.style.height = `${height}px` + const cols = Math.floor(width / (squareSize + gridGap)) + const rows = Math.floor(height / (squareSize + gridGap)) + + const squares = new Float32Array(cols * rows) + for (let i = 0; i < squares.length; i++) { + squares[i] = Math.random() * maxOpacity + } + + return { cols, rows, squares, dpr } + }, + [squareSize, gridGap, maxOpacity] + ) + + const updateSquares = useCallback( + (squares: Float32Array, deltaTime: number) => { + for (let i = 0; i < squares.length; i++) { + if (Math.random() < flickerChance * deltaTime) { + squares[i] = Math.random() * maxOpacity + } + } + }, + [flickerChance, maxOpacity] + ) + + const drawGrid = useCallback( + ( + ctx: CanvasRenderingContext2D, + width: number, + height: number, + cols: number, + rows: number, + squares: Float32Array, + dpr: number + ) => { + ctx.clearRect(0, 0, width, height) + ctx.fillStyle = "transparent" + ctx.fillRect(0, 0, width, height) + + for (let i = 0; i < cols; i++) { + for (let j = 0; j < rows; j++) { + const opacity = squares[i * rows + j] + ctx.fillStyle = `${memoizedColor}${opacity})` + ctx.fillRect( + i * (squareSize + gridGap) * dpr, + j * (squareSize + gridGap) * dpr, + squareSize * dpr, + squareSize * dpr + ) + } + } + }, + [memoizedColor, squareSize, gridGap] + ) + + useEffect(() => { + const canvas = canvasRef.current + const container = containerRef.current + if (!canvas || !container) return + + const ctx = canvas.getContext("2d") + if (!ctx) return + + let animationFrameId: number + let gridParams: ReturnType + + const updateCanvasSize = () => { + const newWidth = width || container.clientWidth + const newHeight = height || container.clientHeight + setCanvasSize({ width: newWidth, height: newHeight }) + gridParams = setupCanvas(canvas, newWidth, newHeight) + } + + updateCanvasSize() + + let lastTime = 0 + const animate = (time: number) => { + if (!isInView) return + + const deltaTime = (time - lastTime) / 1000 + lastTime = time + + updateSquares(gridParams.squares, deltaTime) + drawGrid( + ctx, + canvas.width, + canvas.height, + gridParams.cols, + gridParams.rows, + gridParams.squares, + gridParams.dpr + ) + animationFrameId = requestAnimationFrame(animate) + } + + const resizeObserver = new ResizeObserver(() => { + updateCanvasSize() + }) + + resizeObserver.observe(container) + + const intersectionObserver = new IntersectionObserver( + ([entry]) => { + setIsInView(entry.isIntersecting) + }, + { threshold: 0 } + ) + + intersectionObserver.observe(canvas) + + if (isInView) { + animationFrameId = requestAnimationFrame(animate) + } + + return () => { + cancelAnimationFrame(animationFrameId) + resizeObserver.disconnect() + intersectionObserver.disconnect() + } + }, [setupCanvas, updateSquares, drawGrid, width, height, isInView]) + + return ( +
+ +
+ ) +} diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx index fe0dcd7..ca9269a 100644 --- a/frontend/src/components/workspace/input-box.tsx +++ b/frontend/src/components/workspace/input-box.tsx @@ -89,7 +89,7 @@ export function InputBox({ {selectedModel?.supports_thinking && ( - {context.thinking_enabled ? ( - - ) : ( - - )} + <> + {context.thinking_enabled ? ( + + ) : ( + + )} + + Thinking + + )}