Skip to content

[Draft] Dylan/quick actions animations + UI #310

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 124 additions & 69 deletions apps/web/src/components/artifacts/actions_toolbar/code/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TooltipIconButton } from "@/components/ui/assistant-ui/tooltip-icon-but
import { PortToLanguageOptions } from "./PortToLanguage";
import { ProgrammingLanguageOptions } from "@opencanvas/shared/types";
import { GraphInput } from "@opencanvas/shared/types";
import { AnimatePresence, motion } from "framer-motion";

type SharedComponentProps = {
handleClose: () => void;
Expand Down Expand Up @@ -58,6 +59,7 @@ export function CodeToolBar(props: CodeToolbarProps) {
const { streamMessage } = props;
const [isExpanded, setIsExpanded] = useState(false);
const [activeOption, setActiveOption] = useState<string | null>(null);
const [clickedOption, setClickedOption] = useState<string | null>(null);
const toolbarRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand All @@ -84,32 +86,49 @@ export function CodeToolBar(props: CodeToolbarProps) {
setActiveOption(null);
};

const handleMouseEnter = (event: React.MouseEvent) => {
event.stopPropagation();
if (props.isTextSelected) return;
setIsExpanded(true);
};

const handleMouseLeave = (event: React.MouseEvent) => {
event.stopPropagation();
setIsExpanded(false);
setActiveOption(null);
};

const handleOptionClick = async (
event: React.MouseEvent,
optionId: string
) => {
event.stopPropagation();
setClickedOption(optionId);

if (optionId === "portLanguage") {
setActiveOption(optionId);
return;
}

setIsExpanded(false);
setActiveOption(null);
if (optionId === "addComments") {
await streamMessage({
addComments: true,
});
} else if (optionId === "addLogs") {
await streamMessage({
addLogs: true,
});
} else if (optionId === "fixBugs") {
await streamMessage({
fixBugs: true,
});
}
// Delay closing the toolbar until after the click effect
setTimeout(async () => {
setIsExpanded(false);
setActiveOption(null);
if (optionId === "addComments") {
await streamMessage({
addComments: true,
});
} else if (optionId === "addLogs") {
await streamMessage({
addLogs: true,
});
} else if (optionId === "fixBugs") {
await streamMessage({
fixBugs: true,
});
}
setClickedOption(null);
}, 1000);
};

const handleClose = () => {
Expand All @@ -118,62 +137,98 @@ export function CodeToolBar(props: CodeToolbarProps) {
};

return (
<div
<motion.div
ref={toolbarRef}
className={cn(
"fixed bottom-4 right-4 transition-all duration-300 ease-in-out text-black flex flex-col items-center justify-center bg-white",
isExpanded ? "w-26 min-h-fit rounded-3xl" : "w-12 h-12 rounded-full"
)}
animate={{
height: isExpanded ? "auto" : 48,
borderRadius: isExpanded ? "24px" : "24px",
}}
transition={{
type: "spring",
stiffness: 300,
damping: 30,
}}
className="fixed bottom-4 right-4 flex flex-col items-center justify-center bg-white border-[1px] border-gray-200"
onClick={toggleExpand}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{isExpanded ? (
<div className="flex flex-col gap-3 items-center w-full border-[1px] border-gray-200 rounded-3xl py-4 px-3">
{activeOption && activeOption !== "addEmojis"
? toolbarOptions
.find((option) => option.id === activeOption)
?.component?.({
...props,
handleClose,
})
: toolbarOptions.map((option) => (
<TooltipIconButton
key={option.id}
tooltip={option.tooltip}
variant="ghost"
className="transition-colors w-[36px] h-[36px]"
delayDuration={400}
onClick={async (e) => await handleOptionClick(e, option.id)}
>
{option.icon}
</TooltipIconButton>
))}
</div>
) : (
<TooltipIconButton
tooltip={
props.isTextSelected
? "Quick actions disabled while text is selected"
: "Code tools"
}
variant="outline"
className={cn(
"transition-colors w-[48px] h-[48px] p-0 rounded-xl",
props.isTextSelected
? "cursor-default opacity-50 text-gray-400 hover:bg-background"
: "cursor-pointer"
)}
delayDuration={400}
>
<Code
className={cn(
"w-[26px] h-[26px]",
props.isTextSelected
? "text-gray-400"
: "hover:text-gray-900 transition-colors"
)}
/>
</TooltipIconButton>
)}
</div>
<AnimatePresence>
{isExpanded ? (
<motion.div
key="expanded"
className="flex flex-col gap-3 items-center w-full py-4 px-1 "
>
{activeOption && activeOption !== "addEmojis"
? toolbarOptions
.find((option) => option.id === activeOption)
?.component?.({
...props,
handleClose,
})
: toolbarOptions.map((option) => (
<TooltipIconButton
key={option.id}
tooltip={option.tooltip}
variant="ghost"
className="transition-colors w-[36px] h-[36px] relative overflow-hidden"
delayDuration={200}
onClick={async (e) => await handleOptionClick(e, option.id)}
side="left"
>
{clickedOption === option.id && (
<motion.div
className="absolute inset-0 bg-black"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
transition={{ duration: 0.3 }}
/>
)}
<span
className={cn(
"relative z-10",
clickedOption === option.id && "text-white"
)}
>
{option.icon}
</span>
</TooltipIconButton>
))}
</motion.div>
) : (
<motion.div
key="collapsed"
className="flex items-center justify-center w-full h-full"
>
<TooltipIconButton
tooltip={
props.isTextSelected
? "Quick actions disabled while text is selected"
: "Code tools"
}
variant="outline"
className={cn(
"transition-colors w-[48px] h-[48px] p-0 rounded-xl",
props.isTextSelected
? "cursor-default opacity-50 text-gray-400 hover:bg-background"
: "cursor-pointer"
)}
delayDuration={400}
side="left"
>
<Code
className={cn(
"w-[26px] h-[26px]",
props.isTextSelected
? "text-gray-400"
: "hover:text-gray-900 transition-colors"
)}
/>
</TooltipIconButton>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export function CustomQuickActions(props: CustomQuickActionsProps) {
: "cursor-pointer"
)}
delayDuration={400}
side="left"
>
<WandSparkles
className={cn(
Expand All @@ -201,7 +202,7 @@ export function CustomQuickActions(props: CustomQuickActionsProps) {
/>
</TooltipIconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="max-h-[600px] max-w-[300px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
<DropdownMenuContent className="max-h-[600px] max-w-[200px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
<DropdownMenuLabel>
<TighterText>Custom Quick Actions</TighterText>
</DropdownMenuLabel>
Expand All @@ -212,8 +213,8 @@ export function CustomQuickActions(props: CustomQuickActionsProps) {
<LoaderCircle className="w-4 h-4 animate-spin" />
</span>
) : !customQuickActions?.length ? (
<TighterText className="text-sm text-gray-600 p-2">
No custom quick actions found.
<TighterText className="text-xs text-gray-600 p-2">
No custom actions found
</TighterText>
) : (
<div className="max-h-[450px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
Expand Down
Loading