2025-09-21 11:02:31 -07:00
"use client" ;
import type { UseChatHelpers } from "@ai-sdk/react" ;
import cx from "classnames" ;
2026-03-20 09:37:02 +00:00
import { motion , useMotionValue , useTransform } from "framer-motion" ;
import { WrenchIcon , XIcon } from "lucide-react" ;
2025-09-21 11:02:31 -07:00
import { nanoid } from "nanoid" ;
2024-11-15 12:18:17 -05:00
import {
type Dispatch ,
2024-12-03 17:49:38 +03:00
memo ,
2025-07-03 02:26:34 -07:00
type ReactNode ,
2024-11-15 12:18:17 -05:00
type SetStateAction ,
useEffect ,
useRef ,
useState ,
2025-09-21 11:02:31 -07:00
} from "react" ;
import { useOnClickOutside } from "usehooks-ts" ;
2024-10-30 16:01:24 +05:30
import {
Tooltip ,
TooltipContent ,
TooltipProvider ,
TooltipTrigger ,
2025-09-21 11:02:31 -07:00
} 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" ;
2024-10-30 16:01:24 +05:30
type ToolProps = {
description : string ;
2025-01-27 14:19:47 +05:30
icon : ReactNode ;
2024-10-30 16:01:24 +05:30
selectedTool : string | null ;
setSelectedTool : Dispatch < SetStateAction < string | null > > ;
isToolbarVisible? : boolean ;
setIsToolbarVisible? : Dispatch < SetStateAction < boolean > > ;
isAnimating : boolean ;
2025-09-21 11:02:31 -07:00
sendMessage : UseChatHelpers < ChatMessage > [ "sendMessage" ] ;
2025-01-27 14:19:47 +05:30
onClick : ( {
2025-07-03 02:26:34 -07:00
sendMessage ,
2025-01-27 14:19:47 +05:30
} : {
2025-09-21 11:02:31 -07:00
sendMessage : UseChatHelpers < ChatMessage > [ "sendMessage" ] ;
2025-01-27 14:19:47 +05:30
} ) = > void ;
2024-10-30 16:01:24 +05:30
} ;
const Tool = ( {
description ,
icon ,
selectedTool ,
setSelectedTool ,
isToolbarVisible ,
setIsToolbarVisible ,
isAnimating ,
2025-07-03 02:26:34 -07:00
sendMessage ,
2025-01-27 14:19:47 +05:30
onClick ,
2024-10-30 16:01:24 +05:30
} : ToolProps ) = > {
const [ isHovered , setIsHovered ] = useState ( false ) ;
useEffect ( ( ) = > {
2025-01-27 14:19:47 +05:30
if ( selectedTool !== description ) {
2024-10-30 16:01:24 +05:30
setIsHovered ( false ) ;
}
2025-01-27 14:19:47 +05:30
} , [ selectedTool , description ] ) ;
2024-10-30 16:01:24 +05:30
2024-11-06 19:12:46 +03:00
const handleSelect = ( ) = > {
if ( ! isToolbarVisible && setIsToolbarVisible ) {
setIsToolbarVisible ( true ) ;
return ;
}
if ( ! selectedTool ) {
setIsHovered ( true ) ;
2025-01-27 14:19:47 +05:30
setSelectedTool ( description ) ;
2024-11-06 19:12:46 +03:00
return ;
}
2026-03-13 13:12:33 -07:00
if ( selectedTool === description ) {
2025-01-27 14:19:47 +05:30
setSelectedTool ( null ) ;
2025-07-03 02:26:34 -07:00
onClick ( { sendMessage } ) ;
2026-03-13 13:12:33 -07:00
} else {
setSelectedTool ( description ) ;
2024-11-06 19:12:46 +03:00
}
} ;
2024-10-30 16:01:24 +05:30
return (
< Tooltip open = { isHovered && ! isAnimating } >
2024-11-06 19:12:46 +03:00
< TooltipTrigger asChild >
2024-10-30 16:01:24 +05:30
< motion.div
2025-09-21 11:02:31 -07:00
animate = { { opacity : 1 , transition : { delay : 0.1 } } }
className = { cx ( "rounded-full p-3" , {
"bg-primary text-primary-foreground!" : selectedTool === description ,
2024-10-30 16:01:24 +05:30
} ) }
2025-09-21 11:02:31 -07:00
exit = { {
scale : 0.9 ,
opacity : 0 ,
transition : { duration : 0.1 } ,
} }
initial = { { scale : 1 , opacity : 0 } }
onClick = { ( ) = > {
handleSelect ( ) ;
2024-10-30 16:01:24 +05:30
} }
2025-09-21 12:03:29 +01:00
onHoverEnd = { ( ) = > {
2025-09-21 11:02:31 -07:00
if ( selectedTool !== description ) {
setIsHovered ( false ) ;
}
} }
onHoverStart = { ( ) = > {
setIsHovered ( true ) ;
2025-09-21 12:03:29 +01:00
} }
2024-11-06 19:12:46 +03:00
onKeyDown = { ( event ) = > {
2025-09-21 11:02:31 -07:00
if ( event . key === "Enter" ) {
2024-11-06 19:12:46 +03:00
handleSelect ( ) ;
}
} }
2024-10-30 16:01:24 +05:30
whileHover = { { scale : 1.1 } }
whileTap = { { scale : 0.95 } }
>
2025-01-27 14:19:47 +05:30
{ selectedTool === description ? < ArrowUpIcon / > : icon }
2024-10-30 16:01:24 +05:30
< / motion.div >
< / TooltipTrigger >
< TooltipContent
2025-09-21 11:02:31 -07:00
className = "rounded-2xl bg-foreground p-3 px-4 text-background"
2024-10-30 16:01:24 +05:30
side = "left"
sideOffset = { 16 }
>
{ description }
< / TooltipContent >
< / Tooltip >
) ;
} ;
2025-09-21 11:02:31 -07:00
const randomArr = [ . . . new Array ( 6 ) ] . map ( ( _x ) = > nanoid ( 5 ) ) ;
2024-11-15 12:18:17 -05:00
2024-10-30 16:01:24 +05:30
const ReadingLevelSelector = ( {
setSelectedTool ,
2025-07-03 02:26:34 -07:00
sendMessage ,
2024-10-30 16:01:24 +05:30
isAnimating ,
} : {
setSelectedTool : Dispatch < SetStateAction < string | null > > ;
isAnimating : boolean ;
2025-09-21 11:02:31 -07:00
sendMessage : UseChatHelpers < ChatMessage > [ "sendMessage" ] ;
2024-10-30 16:01:24 +05:30
} ) = > {
const LEVELS = [
2025-09-21 11:02:31 -07:00
"Elementary" ,
"Middle School" ,
"Keep current level" ,
"High School" ,
"College" ,
"Graduate" ,
2024-10-30 16:01:24 +05:30
] ;
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 < boolean > ( false ) ;
useEffect ( ( ) = > {
2025-09-21 11:02:31 -07:00
const unsubscribe = yToLevel . on ( "change" , ( latest ) = > {
2024-10-30 16:01:24 +05:30
const level = Math . min ( 5 , Math . max ( 0 , Math . round ( Math . abs ( latest ) ) ) ) ;
setCurrentLevel ( level ) ;
} ) ;
return ( ) = > unsubscribe ( ) ;
} , [ yToLevel ] ) ;
return (
2025-09-09 15:44:07 -04:00
< div className = "relative flex flex-col items-center justify-end" >
2024-11-15 12:18:17 -05:00
{ randomArr . map ( ( id ) = > (
2024-10-30 16:01:24 +05:30
< motion.div
2025-09-21 12:03:29 +01:00
animate = { { opacity : 1 } }
2025-09-21 11:02:31 -07:00
className = "flex size-[40px] flex-row items-center justify-center"
2025-09-21 12:03:29 +01:00
exit = { { opacity : 0 } }
2025-09-21 11:02:31 -07:00
initial = { { opacity : 0 } }
key = { id }
2024-10-30 16:01:24 +05:30
transition = { { delay : 0.1 } }
>
< div className = "size-2 rounded-full bg-muted-foreground/40" / >
< / motion.div >
) ) }
< TooltipProvider >
< Tooltip open = { ! isAnimating } >
< TooltipTrigger asChild >
< motion.div
className = { cx (
2025-09-21 11:02:31 -07:00
"absolute flex flex-row items-center rounded-full border bg-background p-3" ,
2024-10-30 16:01:24 +05:30
{
2025-09-21 11:02:31 -07:00
"bg-primary text-primary-foreground" : currentLevel !== 2 ,
"bg-background text-foreground" : currentLevel === 2 ,
}
2024-10-30 16:01:24 +05:30
) }
drag = "y"
2025-09-21 11:02:31 -07:00
dragConstraints = { { top : - dragConstraints , bottom : 0 } }
2024-10-30 16:01:24 +05:30
dragElastic = { 0 }
dragMomentum = { false }
onClick = { ( ) = > {
if ( currentLevel !== 2 && hasUserSelectedLevel ) {
2025-07-03 02:26:34 -07:00
sendMessage ( {
2025-09-21 11:02:31 -07:00
role : "user" ,
2025-07-03 02:26:34 -07:00
parts : [
{
2025-09-21 11:02:31 -07:00
type : "text" ,
2025-07-03 02:26:34 -07:00
text : ` Please adjust the reading level to ${ LEVELS [ currentLevel ] } level. ` ,
} ,
] ,
2024-10-30 16:01:24 +05:30
} ) ;
setSelectedTool ( null ) ;
}
} }
2025-09-21 11:02:31 -07:00
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 } }
2024-10-30 16:01:24 +05:30
>
{ currentLevel === 2 ? < SummarizeIcon / > : < ArrowUpIcon / > }
< / motion.div >
< / TooltipTrigger >
< TooltipContent
2025-09-21 11:02:31 -07:00
className = "rounded-2xl bg-foreground p-3 px-4 text-background text-sm"
2024-10-30 16:01:24 +05:30
side = "left"
sideOffset = { 16 }
>
{ LEVELS [ currentLevel ] }
< / TooltipContent >
< / Tooltip >
< / TooltipProvider >
< / div >
) ;
} ;
2024-11-06 19:12:46 +03:00
export const Tools = ( {
selectedTool ,
setSelectedTool ,
2025-07-03 02:26:34 -07:00
sendMessage ,
2024-11-06 19:12:46 +03:00
isAnimating ,
2025-01-27 14:19:47 +05:30
tools ,
2024-10-30 16:01:24 +05:30
} : {
2024-11-06 19:12:46 +03:00
selectedTool : string | null ;
setSelectedTool : Dispatch < SetStateAction < string | null > > ;
2025-09-21 11:02:31 -07:00
sendMessage : UseChatHelpers < ChatMessage > [ "sendMessage" ] ;
2024-11-06 19:12:46 +03:00
isAnimating : boolean ;
2025-09-21 11:02:31 -07:00
tools : ArtifactToolbarItem [ ] ;
2024-10-30 16:01:24 +05:30
} ) = > {
2024-11-06 19:12:46 +03:00
return (
< motion.div
2025-09-21 12:03:29 +01:00
animate = { { opacity : 1 , scale : 1 } }
2025-09-21 11:02:31 -07:00
className = "flex flex-col gap-1.5"
2025-09-21 12:03:29 +01:00
exit = { { opacity : 0 , scale : 0.95 } }
2025-09-21 11:02:31 -07:00
initial = { { opacity : 0 , scale : 0.95 } }
2024-11-06 19:12:46 +03:00
>
2026-03-20 09:37:02 +00:00
{ [ . . . tools ] . reverse ( ) . map ( ( tool ) = > (
< Tool
description = { tool . description }
icon = { tool . icon }
isAnimating = { isAnimating }
key = { tool . description }
onClick = { tool . onClick }
selectedTool = { selectedTool }
sendMessage = { sendMessage }
setSelectedTool = { setSelectedTool }
/ >
) ) }
2024-11-06 19:12:46 +03:00
< / motion.div >
2024-10-30 16:01:24 +05:30
) ;
2024-11-06 19:12:46 +03:00
} ;
2026-03-20 09:37:02 +00:00
const createFixErrorTool = (
consoleOutput : string ,
documentId? : string
) : ArtifactToolbarItem = > ( {
icon : < WrenchIcon className = "size-4" / > ,
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 } ` ,
} ,
] ,
} ) ;
} ,
} ) ;
2024-12-03 17:49:38 +03:00
const PureToolbar = ( {
2026-03-20 09:37:02 +00:00
isToolbarVisible : _isToolbarVisible ,
2024-11-06 19:12:46 +03:00
setIsToolbarVisible ,
2025-07-03 02:26:34 -07:00
sendMessage ,
2025-03-11 15:33:18 -07:00
status ,
2024-11-06 19:12:46 +03:00
stop ,
setMessages ,
2025-02-13 08:25:57 -08:00
artifactKind ,
2026-03-20 09:37:02 +00:00
consoleError ,
documentId ,
artifactActions ,
onClose ,
2024-11-06 19:12:46 +03:00
} : {
isToolbarVisible : boolean ;
setIsToolbarVisible : Dispatch < SetStateAction < boolean > > ;
2025-09-21 11:02:31 -07:00
status : UseChatHelpers < ChatMessage > [ "status" ] ;
sendMessage : UseChatHelpers < ChatMessage > [ "sendMessage" ] ;
stop : UseChatHelpers < ChatMessage > [ "stop" ] ;
setMessages : UseChatHelpers < ChatMessage > [ "setMessages" ] ;
2025-02-13 08:25:57 -08:00
artifactKind : ArtifactKind ;
2026-03-20 09:37:02 +00:00
consoleError? : string ;
documentId? : string ;
artifactActions? : ReactNode ;
onClose ? : ( ) = > void ;
2024-11-06 19:12:46 +03:00
} ) = > {
const toolbarRef = useRef < HTMLDivElement > ( null ) ;
2024-11-15 12:18:17 -05:00
const timeoutRef = useRef < ReturnType < typeof setTimeout > > ( ) ;
2024-11-06 19:12:46 +03:00
const [ selectedTool , setSelectedTool ] = useState < string | null > ( null ) ;
const [ isAnimating , setIsAnimating ] = useState ( false ) ;
useOnClickOutside ( toolbarRef , ( ) = > {
setIsToolbarVisible ( false ) ;
setSelectedTool ( null ) ;
} ) ;
2024-10-30 16:01:24 +05:30
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 ( ( ) = > {
2025-09-21 11:02:31 -07:00
if ( status === "streaming" ) {
2024-10-30 16:01:24 +05:30
setIsToolbarVisible ( false ) ;
}
2025-03-11 15:33:18 -07:00
} , [ status , setIsToolbarVisible ] ) ;
2024-10-30 16:01:24 +05:30
2025-02-13 08:25:57 -08:00
const artifactDefinition = artifactDefinitions . find (
2025-09-21 11:02:31 -07:00
( definition ) = > definition . kind === artifactKind
2025-01-27 14:19:47 +05:30
) ;
2025-02-13 08:25:57 -08:00
if ( ! artifactDefinition ) {
2025-09-21 11:02:31 -07:00
throw new Error ( "Artifact definition not found!" ) ;
2025-01-27 14:19:47 +05:30
}
2026-03-20 09:37:02 +00:00
const toolsByArtifactKind = consoleError
? [
createFixErrorTool ( consoleError , documentId ) ,
. . . artifactDefinition . toolbar . slice ( 1 ) ,
]
: artifactDefinition . toolbar ;
2025-01-27 14:19:47 +05:30
2025-02-13 08:25:57 -08:00
if ( toolsByArtifactKind . length === 0 ) {
2025-01-15 19:59:29 +05:30
return null ;
}
2024-10-30 16:01:24 +05:30
return (
< TooltipProvider delayDuration = { 0 } >
< motion.div
2026-03-20 09:37:02 +00:00
animate = { { opacity : 1 , y : 0 , scale : 1 } }
className = "fixed right-6 bottom-6 z-50 flex cursor-pointer flex-col items-center rounded-3xl border bg-background py-1 shadow-lg"
2024-10-30 16:01:24 +05:30
exit = { { opacity : 0 , y : - 20 , transition : { duration : 0.1 } } }
2025-09-21 11:02:31 -07:00
initial = { { opacity : 0 , y : - 20 , scale : 1 } }
onAnimationComplete = { ( ) = > {
setIsAnimating ( false ) ;
} }
onAnimationStart = { ( ) = > {
setIsAnimating ( true ) ;
2024-10-30 16:01:24 +05:30
} }
onHoverEnd = { ( ) = > {
2025-09-21 11:02:31 -07:00
if ( status === "streaming" ) {
return ;
}
2024-10-30 16:01:24 +05:30
startCloseTimer ( ) ;
} }
2025-09-21 11:02:31 -07:00
onHoverStart = { ( ) = > {
if ( status === "streaming" ) {
return ;
}
cancelCloseTimer ( ) ;
setIsToolbarVisible ( true ) ;
2024-10-30 16:01:24 +05:30
} }
ref = { toolbarRef }
2025-09-21 11:02:31 -07:00
transition = { { type : "spring" , stiffness : 300 , damping : 25 } }
2024-10-30 16:01:24 +05:30
>
2026-03-20 09:37:02 +00:00
{ onClose && (
< motion.div
animate = { { opacity : 1 } }
className = "p-3 text-muted-foreground transition-colors hover:text-foreground"
initial = { { opacity : 0 } }
onClick = { onClose }
>
< XIcon className = "size-4" / >
< / motion.div >
) }
2025-09-21 11:02:31 -07:00
{ status === "streaming" ? (
2024-11-06 19:12:46 +03:00
< motion.div
animate = { { scale : 1.4 } }
2025-09-21 12:03:29 +01:00
className = "p-3"
2025-09-21 11:02:31 -07:00
exit = { { scale : 1 } }
initial = { { scale : 1 } }
key = "stop-icon"
2024-10-30 16:01:24 +05:30
onClick = { ( ) = > {
stop ( ) ;
2025-03-16 18:42:29 -07:00
setMessages ( ( messages ) = > messages ) ;
2024-10-30 16:01:24 +05:30
} }
>
< StopIcon / >
2024-11-06 19:12:46 +03:00
< / motion.div >
2025-09-21 11:02:31 -07:00
) : selectedTool === "adjust-reading-level" ? (
2024-10-30 16:01:24 +05:30
< ReadingLevelSelector
2025-09-21 11:02:31 -07:00
isAnimating = { isAnimating }
2024-11-06 19:12:46 +03:00
key = "reading-level-selector"
2025-07-03 02:26:34 -07:00
sendMessage = { sendMessage }
2024-10-30 16:01:24 +05:30
setSelectedTool = { setSelectedTool }
/ >
) : (
2026-03-20 09:37:02 +00:00
< >
{ artifactActions }
< Tools
isAnimating = { isAnimating }
key = "tools"
selectedTool = { selectedTool }
sendMessage = { sendMessage }
setSelectedTool = { setSelectedTool }
tools = { toolsByArtifactKind }
/ >
< / >
2024-10-30 16:01:24 +05:30
) }
< / motion.div >
< / TooltipProvider >
) ;
} ;
2024-12-03 17:49:38 +03:00
export const Toolbar = memo ( PureToolbar , ( prevProps , nextProps ) = > {
2025-09-21 11:02:31 -07:00
if ( prevProps . status !== nextProps . status ) {
return false ;
}
if ( prevProps . isToolbarVisible !== nextProps . isToolbarVisible ) {
return false ;
}
if ( prevProps . artifactKind !== nextProps . artifactKind ) {
return false ;
}
2026-03-20 09:37:02 +00:00
if ( prevProps . consoleError !== nextProps . consoleError ) {
return false ;
}
if ( prevProps . artifactActions !== nextProps . artifactActions ) {
return false ;
}
if ( prevProps . onClose !== nextProps . onClose ) {
return false ;
}
2024-12-10 17:54:10 +05:30
return true ;
2024-12-03 17:49:38 +03:00
} ) ;