import type { AvatarShape, AvatarStyle } from "@rakazo/contracts"; import { ACTIVE_RUN_STATUSES, avatarIdentitySeed, avatarShapePath, organicAvatarPath, } from "@rakazo/core"; import { memo, useEffect, useState } from "react"; import { Image, View } from "react-native"; import Animated, { cancelAnimation, Easing, useAnimatedProps, useAnimatedStyle, useReducedMotion, useSharedValue, withRepeat, withTiming, } from "react-native-reanimated"; import Svg, { G, Path, Rect } from "react-native-svg"; import { authHeaders, currentApiBase } from "../lib/api"; import { workingAvatarDuration, workingAvatarFrame } from "../lib/avatar-motion"; import { useAvatarStyle } from "./avatar-style"; import { NativeSymbol } from "./native-symbol"; const AnimatedRect = Animated.createAnimatedComponent(Rect); export const BotAvatar = memo(function BotAvatar({ color, size = 54, status, identity, variant, shape, imageSrc, muted = false, }: { color: string; size?: number; status?: string; identity?: string; variant?: AvatarStyle; shape?: AvatarShape | null; imageSrc?: string; muted?: boolean; }) { const isWorking = ACTIVE_RUN_STATUSES.some((activeStatus) => activeStatus === status); const { avatarStyle } = useAvatarStyle(); const visorW = Math.round(size * 0.68); const visorH = Math.round(size * 0.44); const eyeW = Math.max(3, Math.round(size * 0.11)); const eyeH = Math.max(4, Math.round(size * 0.17)); const gap = Math.max(3, Math.round(size * 0.11)); const face = variant ?? avatarStyle; const clipShape = shape ?? "circle"; return ( {imageSrc ? ( ) : face === "organic" ? ( ) : ( {[0, 1].map((eye) => ( ))} )} {isWorking ? ( ) : null} {muted ? ( ) : null} ); }); function AvatarImage({ src, size, shape }: { src: string; size: number; shape: AvatarShape }) { const [headers, setHeaders] = useState>({}); useEffect(() => { void authHeaders().then(setHeaders); }, []); const uri = src.startsWith("http") ? src : `${currentApiBase()}${src}`; return ( ); } function OrganicAvatar({ color, identity, size, isWorking, shape, }: { color: string; identity?: string; size: number; isWorking: boolean; shape?: AvatarShape | null; }) { const seed = avatarIdentitySeed(identity || color || "#8B5CF6"); const progress = useSharedValue(0); const reducedMotion = useReducedMotion(); useEffect(() => { cancelAnimation(progress); progress.value = 0; if (isWorking && !reducedMotion) { progress.value = withRepeat( withTiming(1, { duration: workingAvatarDuration(seed), easing: Easing.linear, }), -1, ); } return () => cancelAnimation(progress); }, [isWorking, progress, reducedMotion, seed]); const bodyStyle = useAnimatedStyle(() => { const frame = workingAvatarFrame(seed, progress.value); return { transform: [ { translateX: (frame.translationX * size) / 120 }, { translateY: (frame.translationY * size) / 120 }, { rotate: `${frame.rotation}deg` }, { scaleX: frame.scaleX }, { scaleY: frame.scaleY }, ], }; }); const leftEyeProps = useAnimatedProps(() => { const frame = workingAvatarFrame(seed, progress.value); return { x: -14 + frame.eyeOffsetX, y: -12 + frame.eyeOffsetY }; }); const rightEyeProps = useAnimatedProps(() => { const frame = workingAvatarFrame(seed, progress.value); return { x: 7 + frame.eyeOffsetX, y: -12 + frame.eyeOffsetY }; }); return ( ); }