"use client"; import type { UseChatHelpers } from "@ai-sdk/react"; import cx from "classnames"; import { motion, useMotionValue, useTransform } from "framer-motion"; import { WrenchIcon, XIcon } from "lucide-react"; import { nanoid } from "nanoid"; import { type Dispatch, memo, type ReactNode, type SetStateAction, useEffect, useRef, useState, } from "react"; import { useOnClickOutside } from "usehooks-ts"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import type { ChatMessage } from "@/lib/types"; import { type ArtifactKind, artifactDefinitions } from "./artifact"; import type { ArtifactToolbarItem } from "./create-artifact"; import { ArrowUpIcon, StopIcon, SummarizeIcon } from "./icons"; 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(null); onClick({ sendMessage }); } else { setSelectedTool(description); } }; return ( { handleSelect(); }} onHoverEnd={() => { if (selectedTool !== description) { setIsHovered(false); } }} onHoverStart={() => { setIsHovered(true); }} onKeyDown={(event) => { if (event.key === "Enter") { handleSelect(); } }} whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }} > {selectedTool === description ? : icon} {description} ); }; const randomArr = [...new 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) => (
))} { if (currentLevel !== 2 && hasUserSelectedLevel) { sendMessage({ role: "user", parts: [ { type: "text", text: `Please adjust the reading level to ${LEVELS[currentLevel]} level.`, }, ], }); setSelectedTool(null); } }} onDragEnd={() => { if (currentLevel === 2) { setSelectedTool(null); } else { setHasUserSelectedLevel(true); } }} onDragStart={() => { setHasUserSelectedLevel(false); }} style={{ y }} transition={{ duration: 0.1 }} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} > {currentLevel === 2 ? : } {LEVELS[currentLevel]}
); }; export const Tools = ({ selectedTool, setSelectedTool, sendMessage, isAnimating, tools, }: { selectedTool: string | null; setSelectedTool: Dispatch>; sendMessage: UseChatHelpers["sendMessage"]; isAnimating: boolean; tools: ArtifactToolbarItem[]; }) => { return ( {[...tools].reverse().map((tool) => ( ))} ); }; const createFixErrorTool = ( consoleOutput: string, documentId?: string ): ArtifactToolbarItem => ({ icon: , description: "Fix error", onClick: ({ sendMessage: send }) => { send({ role: "user", parts: [ { type: "text", text: `Fix the error in the existing script${documentId ? ` (id: ${documentId})` : ""} using updateDocument. Do not create a new script. Console error:\n\n${consoleOutput}`, }, ], }); }, }); const PureToolbar = ({ isToolbarVisible: _isToolbarVisible, setIsToolbarVisible, sendMessage, status, stop, setMessages, artifactKind, consoleError, documentId, artifactActions, onClose, }: { isToolbarVisible: boolean; setIsToolbarVisible: Dispatch>; status: UseChatHelpers["status"]; sendMessage: UseChatHelpers["sendMessage"]; stop: UseChatHelpers["stop"]; setMessages: UseChatHelpers["setMessages"]; artifactKind: ArtifactKind; consoleError?: string; documentId?: string; artifactActions?: ReactNode; onClose?: () => void; }) => { 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 = consoleError ? [ createFixErrorTool(consoleError, documentId), ...artifactDefinition.toolbar.slice(1), ] : artifactDefinition.toolbar; if (toolsByArtifactKind.length === 0) { return null; } return ( { setIsAnimating(false); }} onAnimationStart={() => { setIsAnimating(true); }} onHoverEnd={() => { if (status === "streaming") { return; } startCloseTimer(); }} onHoverStart={() => { if (status === "streaming") { return; } cancelCloseTimer(); setIsToolbarVisible(true); }} ref={toolbarRef} transition={{ type: "spring", stiffness: 300, damping: 25 }} > {onClose && ( )} {status === "streaming" ? ( { stop(); setMessages((messages) => messages); }} > ) : selectedTool === "adjust-reading-level" ? ( ) : ( <> {artifactActions} )} ); }; 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; } if (prevProps.consoleError !== nextProps.consoleError) { return false; } if (prevProps.artifactActions !== nextProps.artifactActions) { return false; } if (prevProps.onClose !== nextProps.onClose) { return false; } return true; });