diff --git a/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.scss b/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.scss index d91a1704..ada0aa3e 100644 --- a/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.scss +++ b/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.scss @@ -26,9 +26,6 @@ } &__pet { - position: absolute; - right: 0; - bottom: 0; width: min(96px, 100vw); height: min(96px, 100vh); max-width: 96px; @@ -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; diff --git a/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx b/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx index ee1f45bd..c7739d51 100644 --- a/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx +++ b/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx @@ -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'; @@ -18,6 +18,8 @@ export const AgentCompanionDesktopPet: React.FC = () => { ); const [mood, setMood] = useState('rest'); const [tasks, setTasks] = useState([]); + const [isHoveringPet, setIsHoveringPet] = useState(false); + const [isDraggingPet, setIsDraggingPet] = useState(false); useEffect(() => { document.documentElement.classList.add('bitfun-agent-companion-window-root'); @@ -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) => { + 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 (
void getCurrentWindow().close()} title="Double-click to close" > @@ -96,11 +113,18 @@ export const AgentCompanionDesktopPet: React.FC = () => { ))} )} - +
setIsHoveringPet(true)} + onPointerLeave={() => setIsHoveringPet(false)} + onPointerDown={startDrag} + > + +
); }; diff --git a/src/web-ui/src/flow_chat/components/ChatInputPixelPet.scss b/src/web-ui/src/flow_chat/components/ChatInputPixelPet.scss index 7ae0a684..7614f45e 100644 --- a/src/web-ui/src/flow_chat/components/ChatInputPixelPet.scss +++ b/src/web-ui/src/flow_chat/components/ChatInputPixelPet.scss @@ -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 @@ -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 ---------- */ @@ -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 ---------- */ @@ -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; @@ -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) { @@ -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; } } diff --git a/src/web-ui/src/flow_chat/components/ChatInputPixelPet.tsx b/src/web-ui/src/flow_chat/components/ChatInputPixelPet.tsx index 2d4b02b3..75c03c6c 100644 --- a/src/web-ui/src/flow_chat/components/ChatInputPixelPet.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInputPixelPet.tsx @@ -29,12 +29,14 @@ import { resolveAgentCompanionPetSrc } from '@/infrastructure/config/services/Ag import './ChatInputPixelPet.scss'; export interface ChatInputPixelPetProps { - mood: ChatInputPetMood; + mood: ChatInputPixelPetMood; className?: string; layout?: 'center' | 'stopRight'; pet?: AgentCompanionPetSelection | null; } +export type ChatInputPixelPetMood = ChatInputPetMood | 'hover' | 'dragging'; + const VIEW_W = 320; const VIEW_H = 204; @@ -202,9 +204,31 @@ function FaceWorking() { ); } -const FACE_ORDER: ChatInputPetMood[] = ['rest', 'analyzing', 'waiting', 'working']; +function FaceHover() { + return ( + + + + + + + ); +} + +function FaceDragging() { + return ( + + + + + + + ); +} + +const FACE_ORDER: ChatInputPixelPetMood[] = ['rest', 'analyzing', 'waiting', 'working', 'hover', 'dragging']; -function FaceFor(mood: ChatInputPetMood) { +function FaceFor(mood: ChatInputPixelPetMood) { switch (mood) { case 'rest': return ; @@ -212,6 +236,10 @@ function FaceFor(mood: ChatInputPetMood) { return ; case 'waiting': return ; + case 'hover': + return ; + case 'dragging': + return ; default: return ; } @@ -271,7 +299,7 @@ export const ChatInputPixelPet: React.FC = ({ }, [pet]); const [transitioning, setTransitioning] = useState(false); - const prevMoodRef = useRef(mood); + const prevMoodRef = useRef(mood); useEffect(() => { if (prevMoodRef.current === mood) return; prevMoodRef.current = mood; @@ -328,8 +356,10 @@ export const ChatInputPixelPet: React.FC = ({ .join(' '); if (pet && petSrc) { - const rowByMood: Record = { + const rowByMood: Record = { rest: 0, + hover: 1, + dragging: 2, analyzing: 8, waiting: 6, working: 7,