'use client'; import { ChatRequestOptions, CreateMessage, Message } from 'ai'; import cx from 'classnames'; import { AnimatePresence, motion, useMotionValue, useTransform, } from 'framer-motion'; import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; import { sanitizeUIMessages } from '@/lib/utils'; import { ArrowUpIcon, MessageIcon, PenIcon, StopIcon, SummarizeIcon, } from './icons'; import useClickOutside from './use-click-outside'; type ToolProps = { type: 'final-polish' | 'request-suggestions' | 'adjust-reading-level'; description: string; icon: JSX.Element; selectedTool: string | null; setSelectedTool: Dispatch>; isToolbarVisible?: boolean; setIsToolbarVisible?: Dispatch>; isAnimating: boolean; append: ( message: Message | CreateMessage, chatRequestOptions?: ChatRequestOptions ) => Promise; }; const Tool = ({ type, description, icon, selectedTool, setSelectedTool, isToolbarVisible, setIsToolbarVisible, isAnimating, append, }: ToolProps) => { const [isHovered, setIsHovered] = useState(false); useEffect(() => { if (selectedTool !== type) { setIsHovered(false); } }, [selectedTool, type]); return ( { setIsHovered(true); }} onHoverEnd={() => { if (selectedTool !== type) setIsHovered(false); }} 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={() => { if (!isToolbarVisible && setIsToolbarVisible) { setIsToolbarVisible(true); return; } if (!selectedTool) { setIsHovered(true); setSelectedTool(type); return; } if (selectedTool !== type) { setSelectedTool(type); } else { if (type === 'final-polish') { append({ role: 'user', content: 'Please add final polish and check for grammar, add section titles for better structure, and ensure everything reads smoothly.', }); setSelectedTool(null); } else if (type === 'request-suggestions') { append({ role: 'user', content: 'Please add suggestions you have that could improve the writing.', }); setSelectedTool(null); } } }} > {selectedTool === type ? : icon} {description} ); }; const ReadingLevelSelector = ({ setSelectedTool, append, isAnimating, }: { setSelectedTool: Dispatch>; isAnimating: boolean; append: ( message: Message | CreateMessage, chatRequestOptions?: ChatRequestOptions ) => Promise; }) => { 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 (
{[...Array(6)].map((_, index) => (
))} { setHasUserSelectedLevel(false); }} onDragEnd={() => { if (currentLevel === 2) { setSelectedTool(null); } else { setHasUserSelectedLevel(true); } }} onClick={() => { if (currentLevel !== 2 && hasUserSelectedLevel) { append({ role: 'user', content: `Please adjust the reading level to ${LEVELS[currentLevel]} level.`, }); setSelectedTool(null); } }} > {currentLevel === 2 ? : } {LEVELS[currentLevel]}
); }; export const Toolbar = ({ isToolbarVisible, setIsToolbarVisible, append, isLoading, stop, setMessages, }: { isToolbarVisible: boolean; setIsToolbarVisible: Dispatch>; isLoading: boolean; append: ( message: Message | CreateMessage, chatRequestOptions?: ChatRequestOptions ) => Promise; stop: () => void; setMessages: Dispatch>; }) => { const toolbarRef = useRef(null); const timeoutRef = useRef(); const [selectedTool, setSelectedTool] = useState(null); const [isAnimating, setIsAnimating] = useState(false); useClickOutside(toolbarRef, () => { setIsToolbarVisible(false); setSelectedTool(null); }); const Tools = ( <> {isToolbarVisible && ( <> } selectedTool={selectedTool} setSelectedTool={setSelectedTool} append={append} isAnimating={isAnimating} /> } selectedTool={selectedTool} setSelectedTool={setSelectedTool} append={append} isAnimating={isAnimating} /> )} } selectedTool={selectedTool} setSelectedTool={setSelectedTool} isToolbarVisible={isToolbarVisible} setIsToolbarVisible={setIsToolbarVisible} append={append} isAnimating={isAnimating} /> ); 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 (isLoading) { setIsToolbarVisible(false); } }, [isLoading, setIsToolbarVisible]); return ( { if (isLoading) return; cancelCloseTimer(); setIsToolbarVisible(true); }} onHoverEnd={() => { if (isLoading) return; startCloseTimer(); }} onAnimationStart={() => { setIsAnimating(true); }} onAnimationComplete={() => { setIsAnimating(false); }} ref={toolbarRef} > {isLoading ? (
{ stop(); setMessages((messages) => sanitizeUIMessages(messages)); }} >
) : selectedTool === 'adjust-reading-level' ? ( ) : ( Tools )}
); };