refactor(components): enhance FindBlocksTool and MorphingTextAnimation

- Updated the `FindBlocksTool` to utilize the new `MorphingTextAnimation` for improved visual feedback.
- Refactored `MorphingTextAnimation` to accept a `text` prop, simplifying its usage and enhancing flexibility.
- Improved the rendering logic in `ChatMessagesContainer` to ensure proper key assignment for dynamic elements.

These changes aim to enhance the user experience by providing better visual transitions and cleaner component interactions.
This commit is contained in:
abhi1992002
2026-02-02 12:23:43 +05:30
parent b5d6853223
commit 6e0fbdea3c
5 changed files with 142 additions and 63 deletions

View File

@@ -10,7 +10,7 @@ import {
MessageResponse,
} from "@/components/ai-elements/message";
import { MessageSquareIcon } from "lucide-react";
import { UIMessage, UIDataTypes, UITools } from "ai";
import { UIMessage, UIDataTypes, UITools, ToolUIPart } from "ai";
import { FindBlocksTool } from "../../tools/FindBlocks/FindBlocks";
interface ChatMessagesContainerProps {
@@ -54,7 +54,10 @@ export const ChatMessagesContainer = ({
);
case "tool-find_block":
return (
<FindBlocksTool message={message} i={i} part={part} />
<FindBlocksTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
default:
return null;

View File

@@ -1,57 +1,52 @@
import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
const MorphingTextAnimationComponent = ({
currentText,
}: {
currentText: string;
}) => {
const letters = currentText.split("");
return (
<motion.span className="inline-flex overflow-hidden">
{letters.map((char, index) => (
<motion.span
key={`${currentText}-${index}`}
initial={{ opacity: 0, y: 8, rotateX: "80deg", filter: "blur(6px)" }}
animate={{ opacity: 1, y: 0, rotateX: "0deg", filter: "blur(0px)" }}
exit={{ opacity: 0, y: -8, rotateX: "-80deg", filter: "blur(6px)" }}
style={{ willChange: "transform" }}
transition={{
delay: 0.015 * index,
interface Props {
text: string;
}
type: "spring",
bounce: 0.5,
}}
className="inline-block"
>
{char === " " ? "\u00A0" : char}
</motion.span>
))}
</motion.span>
);
};
export const MorphingTextAnimation = () => {
const textArray = ["Searching for Twitter blocks", "Found 10 twitter blocks"];
const [currentText, setCurrentText] = useState(textArray[0]);
useEffect(() => {
const interval = setInterval(() => {
setCurrentText(textArray[Math.floor(Math.random() * textArray.length)]);
}, 1000);
return () => clearInterval(interval);
}, []);
export function MorphingTextAnimation({ text }: Props) {
const letters = text.split("");
return (
<div>
<AnimatePresence mode="popLayout" initial={false}>
<motion.div
key={currentText}
className="whitespace-nowrap text-sm text-muted-foreground"
>
<MorphingTextAnimationComponent currentText={currentText} />
<motion.div key={text} className="whitespace-nowrap">
<motion.span className="inline-flex overflow-hidden">
{letters.map((char, index) => (
<motion.span
key={`${text}-${index}`}
initial={{
opacity: 0,
y: 8,
rotateX: "80deg",
filter: "blur(6px)",
}}
animate={{
opacity: 1,
y: 0,
rotateX: "0deg",
filter: "blur(0px)",
}}
exit={{
opacity: 0,
y: -8,
rotateX: "-80deg",
filter: "blur(6px)",
}}
style={{ willChange: "transform" }}
transition={{
delay: 0.015 * index,
type: "spring",
bounce: 0.5,
}}
className="inline-block"
>
{char === " " ? "\u00A0" : char}
</motion.span>
))}
</motion.span>
</motion.div>
</AnimatePresence>
</div>
);
};
}

View File

@@ -1,17 +1,43 @@
import { UIMessage, UIDataTypes, UITools, UIMessagePart } from "ai";
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
import { ToolUIPart } from "ai";
import { getAnimationText, StateIcon } from "./helpers";
export interface FindBlockInput {
query: string;
}
export interface FindBlockOutput {
type: "block_list";
message: string;
session_id: string;
blocks: BlockInfo[];
count: number;
query: string;
usage_hint: string;
}
export interface FindBlockToolPart {
type: string;
toolName?: string;
toolCallId: string;
state: ToolUIPart["state"];
input?: FindBlockInput | unknown;
output?: string | FindBlockOutput | unknown;
title?: string;
}
interface Props {
part: FindBlockToolPart;
}
export function FindBlocksTool({ part }: Props) {
const text = getAnimationText(part);
export const FindBlocksTool = ({
message,
i,
part,
}: {
message: UIMessage<unknown, UIDataTypes, UITools>;
i: number;
part: UIMessagePart<any, any>;
}) => {
return (
<div>
<h1>Find Blocks</h1>
<div className="flex items-center gap-2 py-2 text-sm text-muted-foreground">
<StateIcon state={part.state} />
<MorphingTextAnimation text={text} />
</div>
);
};
}

View File

@@ -0,0 +1,57 @@
import { ToolUIPart } from "ai";
import { FindBlockInput, FindBlockOutput, FindBlockToolPart } from "./FindBlocks";
import { CheckCircleIcon, CircleNotchIcon, XCircleIcon } from "@phosphor-icons/react";
export const getAnimationText = (part: FindBlockToolPart): string => {
switch (part.state) {
case "input-streaming":
return "Searching blocks for you";
case "input-available": {
const query = (part.input as FindBlockInput).query;
return `Finding "${query}" blocks`;
}
case "output-available": {
const parsed = JSON.parse(part.output as string) as FindBlockOutput;
if (parsed) {
return `Found ${parsed.count} "${(part.input as FindBlockInput).query}" blocks`;
}
return "Found blocks";
}
case "output-error":
return "Error finding blocks";
default:
return "Processing";
}
}
export const StateIcon = ({ state }: { state: ToolUIPart["state"] }) => {
switch (state) {
case "input-streaming":
case "input-available":
return (
<CircleNotchIcon
className="h-4 w-4 animate-spin text-muted-foreground"
weight="bold"
/>
);
case "output-available":
return (
<CheckCircleIcon
className="h-4 w-4 text-green-500"
/>
);
case "output-error":
return (
<XCircleIcon
className="h-4 w-4 text-red-500"
/>
);
default:
return null;
}
}

View File

@@ -1,6 +1,5 @@
"use client";
import { MorphingTextAnimation } from "@/app/(platform)/copilot-2/components/MorphingTextAnimation/MorphingTextAnimation";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ArrowDownIcon } from "lucide-react";
@@ -67,7 +66,6 @@ export const ConversationEmptyState = ({
{description}
</p>
)}
<MorphingTextAnimation />
</div>
</>
)}