"use client"; import { cn } from "@/lib/utils"; import { useTamboThread } from "@tambo-ai/react"; import { useEffect, useRef, useState } from "react"; import * as React from "react"; import type { TamboThreadMessage } from "@tambo-ai/react"; /** * Props for the CanvasSpace component * @interface */ interface CanvasSpaceProps { /** Optional CSS class name for custom styling */ className?: string; } /** * A canvas space component that displays rendered components from chat messages. * @component * @example * ```tsx * * ``` */ export function CanvasSpace({ className }: CanvasSpaceProps) { // Access the current Tambo thread context const { thread } = useTamboThread(); // State for managing the currently rendered component const [renderedComponent, setRenderedComponent] = useState(null); // Ref for the scrollable container to enable auto-scrolling const scrollContainerRef = useRef(null); // Track previous thread ID to handle thread changes const previousThreadId = useRef(null); /** * Effect to clear the canvas when switching between threads * Prevents components from previous threads being displayed in new threads */ useEffect(() => { // If there's no thread, or if the thread ID changed, clear the canvas if ( !thread || (previousThreadId.current && previousThreadId.current !== thread.id) ) { setRenderedComponent(null); } // Update the previous thread ID reference previousThreadId.current = thread?.id ?? null; }, [thread]); /** * Effect to handle custom 'tambo:showComponent' events * Allows external triggers to update the rendered component */ useEffect(() => { const handleShowComponent = ( event: CustomEvent<{ messageId: string; component: React.ReactNode }>, ) => { try { setRenderedComponent(event.detail.component); } catch (error) { console.error("Failed to render component:", error); setRenderedComponent(null); } }; window.addEventListener( "tambo:showComponent", handleShowComponent as EventListener, ); return () => { window.removeEventListener( "tambo:showComponent", handleShowComponent as EventListener, ); }; }, []); /** * Effect to automatically display the latest component from thread messages * Updates when thread messages change or new components are added */ useEffect(() => { if (!thread?.messages) { setRenderedComponent(null); return; } const messagesWithComponents = thread.messages.filter( (msg: TamboThreadMessage) => msg.renderedComponent, ); if (messagesWithComponents.length > 0) { const latestMessage = messagesWithComponents[messagesWithComponents.length - 1]; setRenderedComponent(latestMessage.renderedComponent); } }, [thread?.messages]); /** * Effect to auto-scroll to bottom when new components are rendered * Includes a small delay to ensure smooth scrolling */ useEffect(() => { if (scrollContainerRef.current && renderedComponent) { const timeoutId = setTimeout(() => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollTo({ top: scrollContainerRef.current.scrollHeight, behavior: "smooth", }); } }, 100); return () => clearTimeout(timeoutId); } }, [renderedComponent]); return ( {renderedComponent ? ( {renderedComponent} ) : ( Canvas is empty Interactive components will appear here as they are generated )} ); }
Canvas is empty
Interactive components will appear here as they are generated