animate chat dog on new chat and focus input

This commit is contained in:
Cole Goldsmith 2025-11-20 12:31:04 -06:00
parent f7007b625d
commit c76e1d1b91
3 changed files with 85 additions and 18 deletions

View file

@ -20,6 +20,7 @@ interface AssistantMessageProps {
isInactive?: boolean;
animate?: boolean;
delay?: number;
isInitialGreeting?: boolean;
}
export function AssistantMessage({
@ -35,6 +36,7 @@ export function AssistantMessage({
isInactive = false,
animate = true,
delay = 0.2,
isInitialGreeting = false,
}: AssistantMessageProps) {
return (
<motion.div
@ -50,10 +52,31 @@ export function AssistantMessage({
<Message
icon={
<div className="w-8 h-8 flex items-center justify-center flex-shrink-0 select-none">
<DogIcon
className="h-6 w-6 transition-colors duration-300"
disabled={isCompleted || isInactive}
/>
{/* Dog icon with bark animation when greeting */}
<motion.div
animate={
isInitialGreeting
? {
rotate: [-5, -8, -5, 0],
y: [-1, -2, -1, 0],
}
: {}
}
transition={
isInitialGreeting
? {
duration: 0.8,
times: [0, 0.4, 0.7, 1],
ease: "easeInOut",
}
: {}
}
>
<DogIcon
className="h-6 w-6 transition-colors duration-300"
disabled={isCompleted || isInactive}
/>
</motion.div>
</div>
}
actions={
@ -76,20 +99,41 @@ export function AssistantMessage({
onToggle={onToggle}
/>
<div className="relative">
<MarkdownRenderer
className={cn(
"text-sm py-1.5 transition-colors duration-300",
isCompleted ? "text-placeholder-foreground" : "text-foreground",
)}
chatMessage={
isStreaming
? content.trim()
? content +
' <span class="inline-block w-1 h-4 bg-primary ml-1 animate-pulse"></span>'
: '<span class="text-muted-foreground italic">Thinking<span class="thinking-dots"></span></span>'
: content
}
/>
{/* Slide animation for initial greeting */}
{isInitialGreeting ? (
<motion.div
initial={{ opacity: 0, x: -16 }}
animate={{
opacity: [0, 0, 1, 1],
x: [-16, -8, 0, 0],
}}
transition={{
duration: 0.8,
times: [0, 0.3, 0.6, 1],
ease: "easeOut",
}}
>
<MarkdownRenderer
className="text-sm py-1.5 text-foreground"
chatMessage={content}
/>
</motion.div>
) : (
<MarkdownRenderer
className={cn(
"text-sm py-1.5 transition-colors duration-300",
isCompleted ? "text-placeholder-foreground" : "text-foreground",
)}
chatMessage={
isStreaming
? content.trim()
? content +
' <span class="inline-block w-1 h-4 bg-primary ml-1 animate-pulse"></span>'
: '<span class="text-muted-foreground italic">Thinking<span class="thinking-dots"></span></span>'
: content
}
/>
)}
</div>
</Message>
</motion.div>

View file

@ -293,6 +293,11 @@ function ChatPage() {
setIsFilterHighlighted(false);
setLoading(false);
lastLoadedConversationRef.current = null;
// Focus input after a short delay to ensure rendering is complete
setTimeout(() => {
chatInputRef.current?.focusInput();
}, 100);
};
const handleFocusInput = () => {
@ -480,6 +485,11 @@ function ChatPage() {
...prev,
[conversationData.endpoint]: conversationData.response_id,
}));
// Focus input when loading a conversation
setTimeout(() => {
chatInputRef.current?.focusInput();
}, 100);
}
}, [
conversationData,
@ -500,6 +510,11 @@ function ChatPage() {
},
]);
lastLoadedConversationRef.current = null;
// Focus input when starting a new conversation
setTimeout(() => {
chatInputRef.current?.focusInput();
}, 100);
}
}, [placeholderConversation, currentConversationId]);
@ -1061,6 +1076,11 @@ function ChatPage() {
onFork={(e) => handleForkConversation(index, e)}
animate={false}
isInactive={index < messages.length - 1}
isInitialGreeting={
index === 0 &&
messages.length === 1 &&
message.content === "How can I assist?"
}
/>
</div>
),

View file

@ -44,6 +44,7 @@ const DogIcon = ({ disabled = false, stroke, ...props }: DogIconProps) => {
fill={fillColor}
{...props}
>
<title>Dog Icon</title>
<path d="M8 18H2V16H8V18Z" />
<path
fillRule="evenodd"
@ -58,7 +59,9 @@ const DogIcon = ({ disabled = false, stroke, ...props }: DogIconProps) => {
viewBox="0 0 105 77"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>Dog Icon</title>
<defs>
<style dangerouslySetInnerHTML={{ __html: animationCSS }} />
</defs>