'use client'; import cx from 'classnames'; import { AnimatePresence, motion, useMotionValue, useTransform, } from 'framer-motion'; import { type Dispatch, memo, type ReactNode, type SetStateAction, useEffect, useRef, useState, } from 'react'; import { useOnClickOutside } from 'usehooks-ts'; import { nanoid } from 'nanoid'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; import { ArrowUpIcon, StopIcon, SummarizeIcon } from './icons'; import { artifactDefinitions, type ArtifactKind } from './artifact'; import type { ArtifactToolbarItem } from './create-artifact'; import type { UseChatHelpers } from '@ai-sdk/react'; import type { ChatMessage } from '@/lib/types'; type ToolProps = { description: string; icon: ReactNode; selectedTool: string | null; setSelectedTool: Dispatch>; isToolbarVisible?: boolean; setIsToolbarVisible?: Dispatch>; isAnimating: boolean; sendMessage: UseChatHelpers['sendMessage']; onClick: ({ sendMessage, }: { sendMessage: UseChatHelpers['sendMessage']; }) => void; }; const Tool = ({ description, icon, selectedTool, setSelectedTool, isToolbarVisible, setIsToolbarVisible, isAnimating, sendMessage, onClick, }: ToolProps) => { const [isHovered, setIsHovered] = useState(false); useEffect(() => { if (selectedTool !== description) { setIsHovered(false); } }, [selectedTool, description]); const handleSelect = () => { if (!isToolbarVisible && setIsToolbarVisible) { setIsToolbarVisible(true); return; } if (!selectedTool) { setIsHovered(true); setSelectedTool(description); return; } if (selectedTool !== description) { setSelectedTool(description); } else { setSelectedTool(null); onClick({ sendMessage }); } }; return ( { setIsHovered(true); }} onHoverEnd={() => { if (selectedTool !== description) setIsHovered(false); }} onKeyDown={(event) => { if (event.key === 'Enter') { handleSelect(); } }} initial={{ scale: 1, opacity: 0 }} animate={{ opacity: 1, transition: { delay: 0.1 } }} whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }} exit={{ scale: 0.9, opacity: 0, transition: { duration: 0.1 }, }} onClick={() => { handleSelect(); }} > {selectedTool === description ? : icon} {description} ); }; const randomArr = [...Array(6)].map((x) => nanoid(5)); const ReadingLevelSelector = ({ setSelectedTool, sendMessage, isAnimating, }: { setSelectedTool: Dispatch>; isAnimating: boolean; sendMessage: UseChatHelpers['sendMessage']; }) => { const LEVELS = [ 'Elementary', 'Middle School', 'Keep current level', 'High School', 'College', 'Graduate', ]; const y = useMotionValue(-40 * 2); const dragConstraints = 5 * 40 + 2; const yToLevel = useTransform(y, [0, -dragConstraints], [0, 5]); const [currentLevel, setCurrentLevel] = useState(2); const [hasUserSelectedLevel, setHasUserSelectedLevel] = useState(false); useEffect(() => { const unsubscribe = yToLevel.on('change', (latest) => { const level = Math.min(5, Math.max(0, Math.round(Math.abs(latest)))); setCurrentLevel(level); }); return () => unsubscribe(); }, [yToLevel]); return (
{randomArr.map((id) => (
))} { setHasUserSelectedLevel(false); }} onDragEnd={() => { if (currentLevel === 2) { setSelectedTool(null); } else { setHasUserSelectedLevel(true); } }} onClick={() => { if (currentLevel !== 2 && hasUserSelectedLevel) { sendMessage({ role: 'user', parts: [ { type: 'text', text: `Please adjust the reading level to ${LEVELS[currentLevel]} level.`, }, ], }); setSelectedTool(null); } }} > {currentLevel === 2 ? : } {LEVELS[currentLevel]}
); }; export const Tools = ({ isToolbarVisible, selectedTool, setSelectedTool, sendMessage, isAnimating, setIsToolbarVisible, tools, }: { isToolbarVisible: boolean; selectedTool: string | null; setSelectedTool: Dispatch>; sendMessage: UseChatHelpers['sendMessage']; isAnimating: boolean; setIsToolbarVisible: Dispatch>; tools: Array; }) => { const [primaryTool, ...secondaryTools] = tools; return ( {isToolbarVisible && secondaryTools.map((secondaryTool) => ( ))} ); }; const PureToolbar = ({ isToolbarVisible, setIsToolbarVisible, sendMessage, status, stop, setMessages, artifactKind, }: { isToolbarVisible: boolean; setIsToolbarVisible: Dispatch>; status: UseChatHelpers['status']; sendMessage: UseChatHelpers['sendMessage']; stop: UseChatHelpers['stop']; setMessages: UseChatHelpers['setMessages']; artifactKind: ArtifactKind; }) => { const toolbarRef = useRef(null); const timeoutRef = useRef>(); const [selectedTool, setSelectedTool] = useState(null); const [isAnimating, setIsAnimating] = useState(false); useOnClickOutside(toolbarRef, () => { setIsToolbarVisible(false); setSelectedTool(null); }); const startCloseTimer = () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } timeoutRef.current = setTimeout(() => { setSelectedTool(null); setIsToolbarVisible(false); }, 2000); }; const cancelCloseTimer = () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); useEffect(() => { if (status === 'streaming') { setIsToolbarVisible(false); } }, [status, setIsToolbarVisible]); const artifactDefinition = artifactDefinitions.find( (definition) => definition.kind === artifactKind, ); if (!artifactDefinition) { throw new Error('Artifact definition not found!'); } const toolsByArtifactKind = artifactDefinition.toolbar; if (toolsByArtifactKind.length === 0) { return null; } return ( { if (status === 'streaming') return; cancelCloseTimer(); setIsToolbarVisible(true); }} onHoverEnd={() => { if (status === 'streaming') return; startCloseTimer(); }} onAnimationStart={() => { setIsAnimating(true); }} onAnimationComplete={() => { setIsAnimating(false); }} ref={toolbarRef} > {status === 'streaming' ? ( { stop(); setMessages((messages) => messages); }} > ) : selectedTool === 'adjust-reading-level' ? ( ) : ( )} ); }; export const Toolbar = memo(PureToolbar, (prevProps, nextProps) => { if (prevProps.status !== nextProps.status) return false; if (prevProps.isToolbarVisible !== nextProps.isToolbarVisible) return false; if (prevProps.artifactKind !== nextProps.artifactKind) return false; return true; });