Files
moreminimore-vibe/src/components/LoadingBlock.tsx
2025-08-19 15:31:17 -07:00

137 lines
4.2 KiB
TypeScript

import { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import { IpcClient } from "@/ipc/ipc_client";
const customLink = ({
node: _node,
...props
}: {
node?: any;
[key: string]: any;
}) => (
<a
{...props}
onClick={(e) => {
const url = props.href;
if (url) {
e.preventDefault();
IpcClient.getInstance().openExternalUrl(url);
}
}}
/>
);
export const VanillaMarkdownParser = ({ content }: { content: string }) => {
return (
<ReactMarkdown
components={{
a: customLink,
}}
>
{content}
</ReactMarkdown>
);
};
// Chat loader with human-like typing/deleting of rotating messages
function ChatLoader() {
const [currentTextIndex, setCurrentTextIndex] = useState(0);
const [displayText, setDisplayText] = useState("");
const [isDeleting, setIsDeleting] = useState(false);
const [typingSpeed, setTypingSpeed] = useState(100);
const loadingTexts = [
"Preparing your conversation... 🗨️",
"Gathering thoughts... 💭",
"Crafting the perfect response... 🎨",
"Almost there... 🚀",
"Just a moment... ⏳",
"Warming up the neural networks... 🧠",
"Connecting the dots... 🔗",
"Brewing some digital magic... ✨",
"Assembling words with care... 🔤",
"Fine-tuning the response... 🎯",
"Diving into deep thought... 🤿",
"Weaving ideas together... 🕸️",
"Sparking up the conversation... ⚡",
"Polishing the perfect reply... 💎",
];
useEffect(() => {
const currentText = loadingTexts[currentTextIndex];
const timer = window.setTimeout(() => {
if (!isDeleting) {
if (displayText.length < currentText.length) {
setDisplayText(currentText.substring(0, displayText.length + 1));
const randomSpeed = Math.random() * 50 + 30;
const isLongPause = Math.random() > 0.85;
setTypingSpeed(isLongPause ? 300 : randomSpeed);
} else {
setTypingSpeed(1500);
setIsDeleting(true);
}
} else {
if (displayText.length > 0) {
setDisplayText(currentText.substring(0, displayText.length - 1));
setTypingSpeed(30);
} else {
setIsDeleting(false);
setCurrentTextIndex((prev) => (prev + 1) % loadingTexts.length);
setTypingSpeed(500);
}
}
}, typingSpeed);
return () => window.clearTimeout(timer);
}, [displayText, isDeleting, currentTextIndex, typingSpeed]);
const renderFadingText = () => {
return displayText.split("").map((char, index) => {
const opacity = Math.min(
0.8 + (index / (displayText.length || 1)) * 0.2,
1,
);
const isEmoji = /\p{Emoji}/u.test(char);
return (
<span
key={index}
style={{ opacity }}
className={isEmoji ? "inline-block animate-emoji-bounce" : ""}
>
{char}
</span>
);
});
};
return (
<div className="flex flex-col items-center justify-center p-4">
<style>{`
@keyframes blink { from, to { opacity: 0 } 50% { opacity: 1 } }
@keyframes emoji-bounce { 0%, 100% { transform: translateY(0) } 50% { transform: translateY(-2px) } }
@keyframes text-pulse { 0%, 100% { opacity: .85 } 50% { opacity: 1 } }
.animate-blink { animation: blink 1s steps(2, start) infinite; }
.animate-emoji-bounce { animation: emoji-bounce 1.2s ease-in-out infinite; }
.animate-text-pulse { animation: text-pulse 1.8s ease-in-out infinite; }
`}</style>
<div className="text-center animate-text-pulse">
<div className="inline-block">
<p className="text-sm text-gray-500 dark:text-gray-400 font-medium">
{renderFadingText()}
<span className="ml-1 inline-block w-2 h-4 bg-gray-500 dark:bg-gray-400 animate-blink" />
</p>
</div>
</div>
</div>
);
}
interface LoadingBlockProps {
isStreaming?: boolean;
}
// Instead of showing raw thinking content, render the chat loader while streaming.
export function LoadingBlock({ isStreaming = false }: LoadingBlockProps) {
if (!isStreaming) return null;
return <ChatLoader />;
}