Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@
}

&__pet {
position: absolute;
right: 0;
bottom: 0;
width: min(96px, 100vw);
height: min(96px, 100vh);
max-width: 96px;
Expand All @@ -37,6 +34,22 @@
filter: drop-shadow(0 12px 18px rgba(15, 23, 42, 0.18));
}

&__pet-hitbox {
position: absolute;
right: 0;
bottom: 0;
width: min(96px, 100vw);
height: min(96px, 100vh);
display: grid;
place-items: center;
cursor: grab;
pointer-events: auto;

&:active {
cursor: grabbing;
}
}

&__bubbles {
position: absolute;
right: 88px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { listen } from '@tauri-apps/api/event';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { aiExperienceConfigService, type AgentCompanionPetSelection, type AIExperienceSettings } from '@/infrastructure/config/services/AIExperienceConfigService';
import { ChatInputPixelPet } from '@/flow_chat/components/ChatInputPixelPet';
import { ChatInputPixelPet, type ChatInputPixelPetMood } from '@/flow_chat/components/ChatInputPixelPet';
import type { ChatInputPetMood } from '@/flow_chat/utils/chatInputPetMood';
import type { AgentCompanionActivityPayload, AgentCompanionTaskStatus } from '@/flow_chat/utils/agentCompanionActivity';
import { createLogger } from '@/shared/utils/logger';
Expand All @@ -18,6 +18,8 @@ export const AgentCompanionDesktopPet: React.FC = () => {
);
const [mood, setMood] = useState<ChatInputPetMood>('rest');
const [tasks, setTasks] = useState<AgentCompanionTaskStatus[]>([]);
const [isHoveringPet, setIsHoveringPet] = useState(false);
const [isDraggingPet, setIsDraggingPet] = useState(false);

useEffect(() => {
document.documentElement.classList.add('bitfun-agent-companion-window-root');
Expand Down Expand Up @@ -66,16 +68,31 @@ export const AgentCompanionDesktopPet: React.FC = () => {
};
}, []);

const startDrag = () => {
void getCurrentWindow().startDragging().catch(error => {
log.warn('Failed to start Agent companion window drag', error);
});
const startDrag = (event: React.PointerEvent<HTMLDivElement>) => {
if (event.button !== 0) {
return;
}

event.preventDefault();
setIsDraggingPet(true);
void getCurrentWindow().startDragging()
.catch(error => {
log.warn('Failed to start Agent companion window drag', error);
})
.finally(() => {
setIsDraggingPet(false);
});
};

const displayMood: ChatInputPixelPetMood = isDraggingPet
? 'dragging'
: isHoveringPet
? 'hover'
: mood;

return (
<main
className="bitfun-agent-companion-window"
onMouseDown={startDrag}
onDoubleClick={() => void getCurrentWindow().close()}
title="Double-click to close"
>
Expand All @@ -96,11 +113,18 @@ export const AgentCompanionDesktopPet: React.FC = () => {
))}
</div>
)}
<ChatInputPixelPet
mood={mood}
pet={pet}
className="bitfun-agent-companion-window__pet"
/>
<div
className="bitfun-agent-companion-window__pet-hitbox"
onPointerEnter={() => setIsHoveringPet(true)}
onPointerLeave={() => setIsHoveringPet(false)}
onPointerDown={startDrag}
>
<ChatInputPixelPet
mood={displayMood}
pet={pet}
className="bitfun-agent-companion-window__pet"
/>
</div>
</main>
);
};
Expand Down
179 changes: 178 additions & 1 deletion src/web-ui/src/flow_chat/components/ChatInputPixelPet.scss
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@
bitfun-petdex-walk 0.58s steps(6) infinite,
bitfun-petdex-work 0.28s linear infinite;
}

&__petdex--hover {
animation:
bitfun-petdex-walk 0.72s steps(6) infinite,
bitfun-petdex-hover 0.95s ease-in-out infinite;
}

&__petdex--dragging {
animation:
bitfun-petdex-walk 0.48s steps(6) infinite,
bitfun-petdex-dragging 0.24s linear infinite;
}
}

/* Dark theme: keep panda colors, invert decoration color, and apply the
Expand Down Expand Up @@ -134,6 +146,18 @@
bitfun-head-breathe 2.6s ease-in-out infinite,
bitfun-head-shake 0.18s linear infinite;
}

&--hover {
animation:
bitfun-head-breathe 2.1s ease-in-out infinite,
bitfun-head-perk 0.95s ease-in-out infinite;
}

&--dragging {
animation:
bitfun-head-breathe 2.4s ease-in-out infinite,
bitfun-head-carried 0.24s linear infinite;
}
}

/* ---------- Mood overlay layers ---------- */
Expand Down Expand Up @@ -244,6 +268,45 @@
transform-origin: 50% 100%;
transform-box: fill-box;
}

&--hover .bitfun-panda-head__ear--left,
&--hover .bitfun-panda-head__ear--right {
animation: bitfun-ear-perk 0.95s ease-in-out infinite;
transform-origin: 50% 100%;
transform-box: fill-box;
}

&__sparkle {
fill: var(--bitfun-pet-decor);
opacity: 0;
transform-origin: center;
transform-box: fill-box;
animation: bitfun-sparkle-pop 1.2s ease-in-out infinite;

&--b {
animation-delay: 0.22s;
}
}

&--dragging .bitfun-panda-head__paw--front,
&--dragging .bitfun-panda-head__paw--rear {
animation: bitfun-paw-hold 0.48s ease-in-out infinite;
transform-origin: 50% 60%;
transform-box: fill-box;
}

&__drag-line {
fill: none;
stroke: var(--bitfun-pet-decor-soft);
stroke-width: 8;
stroke-linecap: round;
opacity: 0;
animation: bitfun-drag-line-swoop 0.72s ease-in-out infinite;

&--b {
animation-delay: 0.16s;
}
}
}

/* ---------- Mood transition micro-bump ---------- */
Expand Down Expand Up @@ -561,6 +624,86 @@
}
}

@keyframes bitfun-head-perk {
0%,
100% {
transform: translateY(0) rotate(0deg);
}

50% {
transform: translateY(-5px) rotate(-1.6deg);
}
}

@keyframes bitfun-head-carried {
0%,
100% {
transform: rotate(0deg) translateY(0);
}

25% {
transform: rotate(-2.8deg) translateY(-1px);
}

75% {
transform: rotate(2.8deg) translateY(1px);
}
}

@keyframes bitfun-ear-perk {
0%,
100% {
transform: rotate(0deg) translateY(0);
}

50% {
transform: rotate(-8deg) translateY(-2px);
}
}

@keyframes bitfun-sparkle-pop {
0%,
100% {
opacity: 0;
transform: scale(0.55) rotate(0deg);
}

38%,
70% {
opacity: 0.95;
transform: scale(1) rotate(14deg);
}
}

@keyframes bitfun-paw-hold {
0%,
100% {
transform: translateY(0) rotate(0deg);
}

50% {
transform: translateY(-2px) rotate(-3deg);
}
}

@keyframes bitfun-drag-line-swoop {
0% {
opacity: 0;
transform: translateX(10px) scaleX(0.7);
}

35%,
70% {
opacity: 0.72;
transform: translateX(0) scaleX(1);
}

100% {
opacity: 0;
transform: translateX(-10px) scaleX(0.8);
}
}

@keyframes bitfun-petdex-walk {
from {
background-position-x: 0;
Expand Down Expand Up @@ -593,6 +736,32 @@
}
}

@keyframes bitfun-petdex-hover {
0%,
100% {
transform: translateY(0) scale(1);
}

50% {
transform: translateY(-5px) scale(1.04, 0.96);
}
}

@keyframes bitfun-petdex-dragging {
0%,
100% {
transform: rotate(0deg) translateY(0);
}

25% {
transform: rotate(-2.5deg) translateY(-1px);
}

75% {
transform: rotate(2.5deg) translateY(1px);
}
}

/* ---------- Reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
Expand All @@ -609,14 +778,22 @@
.bitfun-panda-head--analyzing,
.bitfun-panda-head--waiting,
.bitfun-panda-head--working,
.bitfun-panda-head--hover,
.bitfun-panda-head--dragging,
.bitfun-panda-head__zzz-glyph,
.bitfun-panda-head--analyzing .bitfun-panda-head__ear--left,
.bitfun-panda-head--analyzing .bitfun-panda-head__ear--right,
.bitfun-panda-head--working .bitfun-panda-head__ear--right,
.bitfun-panda-head--hover .bitfun-panda-head__ear--left,
.bitfun-panda-head--hover .bitfun-panda-head__ear--right,
.bitfun-panda-head--dragging .bitfun-panda-head__paw--front,
.bitfun-panda-head--dragging .bitfun-panda-head__paw--rear,
.bitfun-panda-head--waiting .bitfun-panda-head__paw--front,
.bitfun-panda-head__think-pip,
.bitfun-panda-head__wait-pip,
.bitfun-panda-head__sweat-drop {
.bitfun-panda-head__sweat-drop,
.bitfun-panda-head__sparkle,
.bitfun-panda-head__drag-line {
animation: none !important;
}
}
Loading
Loading