@botuyo/chat-widget-standalone 1.0.63 → 1.0.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Paseo Libre
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paseo Libre
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1 +1 @@
1
- {"version":3,"file":"Avatar3D-LfNJe183.js","sources":["../src/chat-widget/components/Avatar3D.tsx"],"sourcesContent":["/**\n * @package @botuyo/chat-widget\n * Avatar3D Component — Lazy-loaded 3D avatar for voice calls\n *\n * Uses React Three Fiber + @pixiv/three-vrm to render VRM models\n * with emotion-driven blendshapes and idle animations.\n *\n * This component is loaded via React.lazy() — it adds 0KB to the\n * main bundle. Three.js + VRM are only downloaded when a tenant\n * has avatar3dUrl configured.\n */\n\n// React Three Fiber types are resolved via tsconfig\n'use client'\n\nimport { useRef, useEffect, useMemo } from 'react'\nimport { Canvas, useFrame, useThree } from '@react-three/fiber'\nimport { VRMLoaderPlugin, VRMExpressionPresetName, VRM } from '@pixiv/three-vrm'\nimport * as THREE from 'three'\nimport { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'\n\n// ═══════════════════════════════════════\n// TYPES\n// ═══════════════════════════════════════\n\ntype CallState = 'idle' | 'connecting' | 'listening' | 'speaking' | 'thinking'\n\ninterface Avatar3DProps {\n modelUrl: string\n emotion: string | null\n callState: CallState\n audioLevel: number\n primaryColor: string\n size: number\n}\n\n// ═══════════════════════════════════════\n// EMOTION → VRM BLENDSHAPE MAPPING\n// ═══════════════════════════════════════\n\ninterface EmotionConfig {\n expressions: Record<string, number> // VRM expression name → weight 0-1\n headRotation?: [number, number, number] // euler XYZ in radians\n}\n\nconst EMOTION_MAP: Record<string, EmotionConfig> = {\n default: {\n expressions: { [VRMExpressionPresetName.Neutral]: 1 },\n },\n happy: {\n expressions: { [VRMExpressionPresetName.Happy]: 0.8 },\n },\n angry: {\n expressions: { [VRMExpressionPresetName.Angry]: 0.7 },\n headRotation: [0.05, 0, 0],\n },\n sorry: {\n expressions: { [VRMExpressionPresetName.Sad]: 0.6 },\n headRotation: [-0.08, 0, 0],\n },\n confused: {\n expressions: { [VRMExpressionPresetName.Sad]: 0.3 },\n headRotation: [0, 0, 0.08],\n },\n love: {\n expressions: { [VRMExpressionPresetName.Happy]: 0.9, [VRMExpressionPresetName.Relaxed]: 0.4 },\n },\n thinking: {\n expressions: { [VRMExpressionPresetName.Neutral]: 0.6 },\n headRotation: [0.06, -0.1, 0],\n },\n writing: {\n expressions: { [VRMExpressionPresetName.Neutral]: 0.8 },\n headRotation: [-0.05, 0, 0],\n },\n wink: {\n expressions: { [VRMExpressionPresetName.Happy]: 0.5, [VRMExpressionPresetName.Blink]: 0.9 },\n },\n}\n\n// ═══════════════════════════════════════\n// VRM MODEL COMPONENT (inside Canvas)\n// ═══════════════════════════════════════\n\ninterface VRMModelProps {\n url: string\n emotion: string | null\n callState: CallState\n audioLevel: number\n}\n\nfunction VRMModel({ url, emotion, callState, audioLevel }: VRMModelProps) {\n const vrmRef = useRef<VRM | null>(null)\n const glbSceneRef = useRef<THREE.Group | null>(null)\n const mixerRef = useRef<THREE.AnimationMixer | null>(null)\n const baseRotationY = useRef(0)\n const baseScale = useRef(1)\n const blinkTimer = useRef(0)\n const nextBlinkAt = useRef(3.5) // Initial value, randomized in useFrame\n const breathPhase = useRef(0)\n const currentExpressions = useRef<Record<string, number>>({})\n const targetExpressions = useRef<Record<string, number>>({})\n const headTarget = useRef(new THREE.Euler(0, 0, 0))\n const baseY = useRef(0)\n const { scene, camera } = useThree()\n\n // Load model using Three.js GLTFLoader (with VRM plugin for VRM models)\n useEffect(() => {\n const loader = new GLTFLoader()\n loader.register((parser) => new VRMLoaderPlugin(parser) as any)\n\n loader.load(\n url,\n (gltf) => {\n const vrm = (gltf as any).userData?.vrm as VRM | undefined\n if (vrm) {\n // ── VRM MODEL PATH ──\n vrmRef.current = vrm\n vrm.scene.rotation.y = Math.PI\n const box = new THREE.Box3().setFromObject(vrm.scene)\n const center = box.getCenter(new THREE.Vector3())\n const height = box.getSize(new THREE.Vector3()).y\n vrm.scene.position.y = -center.y - height * 0.05\n baseY.current = vrm.scene.position.y\n scene.add(vrm.scene)\n } else {\n // ── PLAIN GLB FALLBACK ──\n console.info('[Avatar3D] Plain GLB model detected, using fallback animations')\n const model = gltf.scene\n\n // Force world matrix update so bbox is accurate\n model.updateMatrixWorld(true)\n const box = new THREE.Box3().setFromObject(model)\n const center = box.getCenter(new THREE.Vector3())\n const size = box.getSize(new THREE.Vector3())\n\n // Create pivot wrapper for proper rotation around geometric center\n const pivot = new THREE.Group()\n\n // Center model geometry at pivot origin\n model.position.sub(center)\n pivot.add(model)\n\n // Don't rotate the model — move the CAMERA to the front instead\n baseRotationY.current = 0\n\n glbSceneRef.current = pivot\n baseY.current = 0\n baseScale.current = 1\n scene.add(pivot)\n\n // Frame the HEAD: camera at -Z (model faces -Z based on tests)\n const fov = (camera as THREE.PerspectiveCamera).fov * (Math.PI / 180)\n const headY = size.y * 0.35\n const cameraZ = (size.y * 0.5) / Math.tan(fov / 2)\n // Camera on the -Z side to see the model's FRONT\n camera.position.set(0, headY, -cameraZ)\n camera.lookAt(0, headY, 0)\n\n // Play embedded animations if available\n if (gltf.animations.length > 0) {\n const mixer = new THREE.AnimationMixer(model)\n mixerRef.current = mixer\n gltf.animations.forEach((clip) => {\n mixer.clipAction(clip).play()\n })\n }\n }\n },\n undefined,\n (error) => {\n console.error('[Avatar3D] Failed to load model:', error)\n },\n )\n\n return () => {\n if (vrmRef.current) {\n scene.remove(vrmRef.current.scene)\n vrmRef.current = null\n }\n if (glbSceneRef.current) {\n scene.remove(glbSceneRef.current)\n glbSceneRef.current = null\n }\n if (mixerRef.current) {\n mixerRef.current.stopAllAction()\n mixerRef.current = null\n }\n }\n }, [url, scene])\n\n // Update target expressions when emotion changes (VRM only)\n useEffect(() => {\n const config = EMOTION_MAP[emotion || 'default'] || EMOTION_MAP.default\n targetExpressions.current = config.expressions\n if (config.headRotation) {\n headTarget.current.set(...config.headRotation)\n } else {\n headTarget.current.set(0, 0, 0)\n }\n }, [emotion])\n\n // Animation loop\n useFrame((_, delta) => {\n const dt = Math.min(delta, 0.1) // Cap delta to avoid jumps\n breathPhase.current += dt * 1.2\n\n // ── GLB FALLBACK ANIMATION ──\n const glb = glbSceneRef.current\n if (glb) {\n // Update animation mixer if present\n if (mixerRef.current) {\n mixerRef.current.update(dt)\n }\n\n // Gentle breathing bob\n const breathOffset = Math.sin(breathPhase.current) * 0.003\n glb.position.y = baseY.current + breathOffset\n\n // Subtle idle rotation (relative to base facing direction)\n const idleSway = Math.sin(breathPhase.current * 0.3) * 0.02\n glb.rotation.y = baseRotationY.current + idleSway\n\n // Audio-reactive scale pulse when speaking\n if (callState === 'speaking' && audioLevel > 0.01) {\n const pulse = 1 + audioLevel * 0.03\n const targetScale = baseScale.current * pulse\n const currentScale = glb.scale.x\n glb.scale.setScalar(THREE.MathUtils.lerp(currentScale, targetScale, 0.1))\n } else {\n // Return to base scale smoothly\n const currentScale = glb.scale.x\n if (Math.abs(currentScale - baseScale.current) > 0.001) {\n glb.scale.setScalar(THREE.MathUtils.lerp(currentScale, baseScale.current, 0.05))\n }\n }\n\n return // Skip VRM-specific code\n }\n\n // ── VRM ANIMATION (original code) ──\n const vrm = vrmRef.current\n if (!vrm) return\n\n // ── BREATHING ──\n const breathOffset = Math.sin(breathPhase.current) * 0.003\n vrm.scene.position.y = baseY.current + breathOffset\n\n // ── BLINKING ──\n blinkTimer.current += dt\n if (blinkTimer.current >= nextBlinkAt.current) {\n const blinkProgress = (blinkTimer.current - nextBlinkAt.current) / 0.15\n if (blinkProgress < 1) {\n vrm.expressionManager?.setValue(VRMExpressionPresetName.Blink, blinkProgress)\n } else if (blinkProgress < 2) {\n vrm.expressionManager?.setValue(VRMExpressionPresetName.Blink, 2 - blinkProgress)\n } else {\n vrm.expressionManager?.setValue(VRMExpressionPresetName.Blink, 0)\n blinkTimer.current = 0\n nextBlinkAt.current = Math.random() * 4 + 2\n }\n }\n\n // ── EMOTION BLENDING ──\n const lerpSpeed = 4 * dt\n const allKeys = new Set([\n ...Object.keys(currentExpressions.current),\n ...Object.keys(targetExpressions.current),\n ])\n\n for (const key of allKeys) {\n const current = currentExpressions.current[key] || 0\n const target = targetExpressions.current[key] || 0\n const next = THREE.MathUtils.lerp(current, target, lerpSpeed)\n currentExpressions.current[key] = next\n\n // Don't override blink during active blink animation\n if (key === VRMExpressionPresetName.Blink && blinkTimer.current >= nextBlinkAt.current) continue\n\n vrm.expressionManager?.setValue(key, next)\n }\n\n // ── SPEAKING MOUTH ──\n if (callState === 'speaking' && audioLevel > 0.01) {\n const mouthTarget = Math.min(audioLevel * 1.5, 0.8)\n const currentMouth = vrm.expressionManager?.getValue(VRMExpressionPresetName.Aa) || 0\n const smoothMouth = THREE.MathUtils.lerp(currentMouth, mouthTarget, 8 * dt)\n vrm.expressionManager?.setValue(VRMExpressionPresetName.Aa, smoothMouth)\n } else {\n const currentMouth = vrm.expressionManager?.getValue(VRMExpressionPresetName.Aa) || 0\n if (currentMouth > 0.01) {\n vrm.expressionManager?.setValue(VRMExpressionPresetName.Aa, currentMouth * 0.9)\n }\n }\n\n // ── HEAD MOVEMENT ──\n if (vrm.humanoid) {\n const head = vrm.humanoid.getNormalizedBoneNode('head')\n if (head) {\n const idleSwayX = Math.sin(breathPhase.current * 0.3) * 0.01\n const idleSwayZ = Math.cos(breathPhase.current * 0.2) * 0.005\n\n const targetX = headTarget.current.x + idleSwayX\n const targetY = headTarget.current.y\n const targetZ = headTarget.current.z + idleSwayZ\n\n head.rotation.x = THREE.MathUtils.lerp(head.rotation.x, targetX, 3 * dt)\n head.rotation.y = THREE.MathUtils.lerp(head.rotation.y, targetY, 3 * dt)\n head.rotation.z = THREE.MathUtils.lerp(head.rotation.z, targetZ, 3 * dt)\n }\n }\n\n // ── CALL STATE REACTIONS ──\n if (callState === 'listening' && vrm.humanoid) {\n const head = vrm.humanoid.getNormalizedBoneNode('head')\n if (head) {\n head.rotation.z = THREE.MathUtils.lerp(head.rotation.z, 0.03, 2 * dt)\n }\n }\n\n // Update VRM (required for expression changes to take effect)\n vrm.update(dt)\n })\n\n return null\n}\n\n// ═══════════════════════════════════════\n// AUTO CAMERA FRAMING\n// ═══════════════════════════════════════\n\nfunction CameraSetup() {\n const { camera } = useThree()\n useEffect(() => {\n // Default camera position — will be overridden by model loader for GLB\n // For VRM: fixed head-level framing\n camera.position.set(0, 0.1, 0.6)\n camera.lookAt(0, 0.1, 0)\n }, [camera])\n return null\n}\n\n// ═══════════════════════════════════════\n// MAIN AVATAR 3D COMPONENT (exported)\n// ═══════════════════════════════════════\n\nexport default function Avatar3D({\n modelUrl,\n emotion,\n callState,\n audioLevel,\n primaryColor,\n size,\n}: Avatar3DProps) {\n const glowIntensity = useMemo(() => {\n switch (callState) {\n case 'speaking': return 0.6\n case 'listening': return 0.4\n case 'thinking': return 0.3\n default: return 0.15\n }\n }, [callState])\n\n const isActive = callState === 'listening' || callState === 'speaking'\n\n return (\n <div className=\"relative flex flex-col items-center gap-4\">\n {/* Glow ring behind canvas */}\n <div\n className=\"relative rounded-full overflow-hidden\"\n style={{\n width: `${size}px`,\n height: `${size}px`,\n boxShadow: isActive\n ? `0 0 0 3px ${primaryColor}50, 0 0 30px ${primaryColor}${Math.floor(glowIntensity * 255).toString(16).padStart(2, '0')}, 0 0 60px ${primaryColor}20`\n : `0 0 0 2px ${primaryColor}25`,\n transition: 'box-shadow 0.5s ease',\n }}\n >\n <Canvas\n style={{ width: '100%', height: '100%', background: 'transparent' }}\n gl={{ alpha: true, antialias: true, preserveDrawingBuffer: false }}\n camera={{ fov: 30, near: 0.01, far: 10 }}\n dpr={[1, 2]}\n >\n <CameraSetup />\n\n {/* Lighting — soft studio setup */}\n {/* @ts-ignore R3F IntrinsicElements conflict */}\n <ambientLight intensity={0.6} />\n {/* @ts-ignore */}\n <directionalLight position={[1, 2, 3]} intensity={0.8} color=\"#ffffff\" />\n {/* @ts-ignore */}\n <directionalLight position={[-1, 1, -1]} intensity={0.3} color=\"#b4c6e7\" />\n\n {/* Rim light for depth — matches primary color */}\n {/* @ts-ignore */}\n <pointLight\n position={[0, 0.5, -0.5]}\n intensity={glowIntensity * 2}\n color={primaryColor}\n distance={3}\n />\n\n <VRMModel\n url={modelUrl}\n emotion={emotion}\n callState={callState}\n audioLevel={audioLevel}\n />\n </Canvas>\n </div>\n </div>\n )\n}\n"],"names":["EMOTION_MAP","default","expressions","VRMExpressionPresetName","Neutral","happy","Happy","angry","Angry","headRotation","sorry","Sad","confused","love","Relaxed","thinking","writing","wink","Blink","VRMModel","url","emotion","callState","audioLevel","vrmRef","useRef","glbSceneRef","mixerRef","baseRotationY","baseScale","blinkTimer","nextBlinkAt","breathPhase","currentExpressions","targetExpressions","headTarget","THREE","Euler","baseY","scene","camera","useThree","useEffect","loader","GLTFLoader","register","parser","VRMLoaderPlugin","load","gltf","vrm","userData","current","rotation","y","Math","PI","box","Box3","setFromObject","center","getCenter","Vector3","height","getSize","position","add","console","info","model","updateMatrixWorld","size","pivot","Group","sub","fov","headY","cameraZ","tan","set","lookAt","animations","length","mixer","AnimationMixer","forEach","clip","clipAction","play","error","remove","stopAllAction","config","useFrame","_","delta","dt","min","glb","update","breathOffset","sin","idleSway","pulse","targetScale","currentScale","scale","x","setScalar","MathUtils","lerp","abs","blinkProgress","expressionManager","setValue","random","lerpSpeed","allKeys","Set","Object","keys","key","target","next","mouthTarget","currentMouth","getValue","Aa","smoothMouth","humanoid","head","getNormalizedBoneNode","idleSwayX","idleSwayZ","cos","targetX","targetY","targetZ","z","CameraSetup","Avatar3D","modelUrl","primaryColor","glowIntensity","useMemo","jsx","className","children","style","width","boxShadow","floor","toString","padStart","transition","jsxs","Canvas","background","gl","alpha","antialias","preserveDrawingBuffer","near","far","dpr","intensity","color","distance"],"mappings":"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"}
1
+ {"version":3,"file":"Avatar3D-LfNJe183.js","sources":["../src/chat-widget/components/Avatar3D.tsx"],"sourcesContent":["/**\r\n * @package @botuyo/chat-widget\r\n * Avatar3D Component — Lazy-loaded 3D avatar for voice calls\r\n *\r\n * Uses React Three Fiber + @pixiv/three-vrm to render VRM models\r\n * with emotion-driven blendshapes and idle animations.\r\n *\r\n * This component is loaded via React.lazy() — it adds 0KB to the\r\n * main bundle. Three.js + VRM are only downloaded when a tenant\r\n * has avatar3dUrl configured.\r\n */\r\n\r\n// React Three Fiber types are resolved via tsconfig\r\n'use client'\r\n\r\nimport { useRef, useEffect, useMemo } from 'react'\r\nimport { Canvas, useFrame, useThree } from '@react-three/fiber'\r\nimport { VRMLoaderPlugin, VRMExpressionPresetName, VRM } from '@pixiv/three-vrm'\r\nimport * as THREE from 'three'\r\nimport { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'\r\n\r\n// ═══════════════════════════════════════\r\n// TYPES\r\n// ═══════════════════════════════════════\r\n\r\ntype CallState = 'idle' | 'connecting' | 'listening' | 'speaking' | 'thinking'\r\n\r\ninterface Avatar3DProps {\r\n modelUrl: string\r\n emotion: string | null\r\n callState: CallState\r\n audioLevel: number\r\n primaryColor: string\r\n size: number\r\n}\r\n\r\n// ═══════════════════════════════════════\r\n// EMOTION → VRM BLENDSHAPE MAPPING\r\n// ═══════════════════════════════════════\r\n\r\ninterface EmotionConfig {\r\n expressions: Record<string, number> // VRM expression name → weight 0-1\r\n headRotation?: [number, number, number] // euler XYZ in radians\r\n}\r\n\r\nconst EMOTION_MAP: Record<string, EmotionConfig> = {\r\n default: {\r\n expressions: { [VRMExpressionPresetName.Neutral]: 1 },\r\n },\r\n happy: {\r\n expressions: { [VRMExpressionPresetName.Happy]: 0.8 },\r\n },\r\n angry: {\r\n expressions: { [VRMExpressionPresetName.Angry]: 0.7 },\r\n headRotation: [0.05, 0, 0],\r\n },\r\n sorry: {\r\n expressions: { [VRMExpressionPresetName.Sad]: 0.6 },\r\n headRotation: [-0.08, 0, 0],\r\n },\r\n confused: {\r\n expressions: { [VRMExpressionPresetName.Sad]: 0.3 },\r\n headRotation: [0, 0, 0.08],\r\n },\r\n love: {\r\n expressions: { [VRMExpressionPresetName.Happy]: 0.9, [VRMExpressionPresetName.Relaxed]: 0.4 },\r\n },\r\n thinking: {\r\n expressions: { [VRMExpressionPresetName.Neutral]: 0.6 },\r\n headRotation: [0.06, -0.1, 0],\r\n },\r\n writing: {\r\n expressions: { [VRMExpressionPresetName.Neutral]: 0.8 },\r\n headRotation: [-0.05, 0, 0],\r\n },\r\n wink: {\r\n expressions: { [VRMExpressionPresetName.Happy]: 0.5, [VRMExpressionPresetName.Blink]: 0.9 },\r\n },\r\n}\r\n\r\n// ═══════════════════════════════════════\r\n// VRM MODEL COMPONENT (inside Canvas)\r\n// ═══════════════════════════════════════\r\n\r\ninterface VRMModelProps {\r\n url: string\r\n emotion: string | null\r\n callState: CallState\r\n audioLevel: number\r\n}\r\n\r\nfunction VRMModel({ url, emotion, callState, audioLevel }: VRMModelProps) {\r\n const vrmRef = useRef<VRM | null>(null)\r\n const glbSceneRef = useRef<THREE.Group | null>(null)\r\n const mixerRef = useRef<THREE.AnimationMixer | null>(null)\r\n const baseRotationY = useRef(0)\r\n const baseScale = useRef(1)\r\n const blinkTimer = useRef(0)\r\n const nextBlinkAt = useRef(3.5) // Initial value, randomized in useFrame\r\n const breathPhase = useRef(0)\r\n const currentExpressions = useRef<Record<string, number>>({})\r\n const targetExpressions = useRef<Record<string, number>>({})\r\n const headTarget = useRef(new THREE.Euler(0, 0, 0))\r\n const baseY = useRef(0)\r\n const { scene, camera } = useThree()\r\n\r\n // Load model using Three.js GLTFLoader (with VRM plugin for VRM models)\r\n useEffect(() => {\r\n const loader = new GLTFLoader()\r\n loader.register((parser) => new VRMLoaderPlugin(parser) as any)\r\n\r\n loader.load(\r\n url,\r\n (gltf) => {\r\n const vrm = (gltf as any).userData?.vrm as VRM | undefined\r\n if (vrm) {\r\n // ── VRM MODEL PATH ──\r\n vrmRef.current = vrm\r\n vrm.scene.rotation.y = Math.PI\r\n const box = new THREE.Box3().setFromObject(vrm.scene)\r\n const center = box.getCenter(new THREE.Vector3())\r\n const height = box.getSize(new THREE.Vector3()).y\r\n vrm.scene.position.y = -center.y - height * 0.05\r\n baseY.current = vrm.scene.position.y\r\n scene.add(vrm.scene)\r\n } else {\r\n // ── PLAIN GLB FALLBACK ──\r\n console.info('[Avatar3D] Plain GLB model detected, using fallback animations')\r\n const model = gltf.scene\r\n\r\n // Force world matrix update so bbox is accurate\r\n model.updateMatrixWorld(true)\r\n const box = new THREE.Box3().setFromObject(model)\r\n const center = box.getCenter(new THREE.Vector3())\r\n const size = box.getSize(new THREE.Vector3())\r\n\r\n // Create pivot wrapper for proper rotation around geometric center\r\n const pivot = new THREE.Group()\r\n\r\n // Center model geometry at pivot origin\r\n model.position.sub(center)\r\n pivot.add(model)\r\n\r\n // Don't rotate the model — move the CAMERA to the front instead\r\n baseRotationY.current = 0\r\n\r\n glbSceneRef.current = pivot\r\n baseY.current = 0\r\n baseScale.current = 1\r\n scene.add(pivot)\r\n\r\n // Frame the HEAD: camera at -Z (model faces -Z based on tests)\r\n const fov = (camera as THREE.PerspectiveCamera).fov * (Math.PI / 180)\r\n const headY = size.y * 0.35\r\n const cameraZ = (size.y * 0.5) / Math.tan(fov / 2)\r\n // Camera on the -Z side to see the model's FRONT\r\n camera.position.set(0, headY, -cameraZ)\r\n camera.lookAt(0, headY, 0)\r\n\r\n // Play embedded animations if available\r\n if (gltf.animations.length > 0) {\r\n const mixer = new THREE.AnimationMixer(model)\r\n mixerRef.current = mixer\r\n gltf.animations.forEach((clip) => {\r\n mixer.clipAction(clip).play()\r\n })\r\n }\r\n }\r\n },\r\n undefined,\r\n (error) => {\r\n console.error('[Avatar3D] Failed to load model:', error)\r\n },\r\n )\r\n\r\n return () => {\r\n if (vrmRef.current) {\r\n scene.remove(vrmRef.current.scene)\r\n vrmRef.current = null\r\n }\r\n if (glbSceneRef.current) {\r\n scene.remove(glbSceneRef.current)\r\n glbSceneRef.current = null\r\n }\r\n if (mixerRef.current) {\r\n mixerRef.current.stopAllAction()\r\n mixerRef.current = null\r\n }\r\n }\r\n }, [url, scene])\r\n\r\n // Update target expressions when emotion changes (VRM only)\r\n useEffect(() => {\r\n const config = EMOTION_MAP[emotion || 'default'] || EMOTION_MAP.default\r\n targetExpressions.current = config.expressions\r\n if (config.headRotation) {\r\n headTarget.current.set(...config.headRotation)\r\n } else {\r\n headTarget.current.set(0, 0, 0)\r\n }\r\n }, [emotion])\r\n\r\n // Animation loop\r\n useFrame((_, delta) => {\r\n const dt = Math.min(delta, 0.1) // Cap delta to avoid jumps\r\n breathPhase.current += dt * 1.2\r\n\r\n // ── GLB FALLBACK ANIMATION ──\r\n const glb = glbSceneRef.current\r\n if (glb) {\r\n // Update animation mixer if present\r\n if (mixerRef.current) {\r\n mixerRef.current.update(dt)\r\n }\r\n\r\n // Gentle breathing bob\r\n const breathOffset = Math.sin(breathPhase.current) * 0.003\r\n glb.position.y = baseY.current + breathOffset\r\n\r\n // Subtle idle rotation (relative to base facing direction)\r\n const idleSway = Math.sin(breathPhase.current * 0.3) * 0.02\r\n glb.rotation.y = baseRotationY.current + idleSway\r\n\r\n // Audio-reactive scale pulse when speaking\r\n if (callState === 'speaking' && audioLevel > 0.01) {\r\n const pulse = 1 + audioLevel * 0.03\r\n const targetScale = baseScale.current * pulse\r\n const currentScale = glb.scale.x\r\n glb.scale.setScalar(THREE.MathUtils.lerp(currentScale, targetScale, 0.1))\r\n } else {\r\n // Return to base scale smoothly\r\n const currentScale = glb.scale.x\r\n if (Math.abs(currentScale - baseScale.current) > 0.001) {\r\n glb.scale.setScalar(THREE.MathUtils.lerp(currentScale, baseScale.current, 0.05))\r\n }\r\n }\r\n\r\n return // Skip VRM-specific code\r\n }\r\n\r\n // ── VRM ANIMATION (original code) ──\r\n const vrm = vrmRef.current\r\n if (!vrm) return\r\n\r\n // ── BREATHING ──\r\n const breathOffset = Math.sin(breathPhase.current) * 0.003\r\n vrm.scene.position.y = baseY.current + breathOffset\r\n\r\n // ── BLINKING ──\r\n blinkTimer.current += dt\r\n if (blinkTimer.current >= nextBlinkAt.current) {\r\n const blinkProgress = (blinkTimer.current - nextBlinkAt.current) / 0.15\r\n if (blinkProgress < 1) {\r\n vrm.expressionManager?.setValue(VRMExpressionPresetName.Blink, blinkProgress)\r\n } else if (blinkProgress < 2) {\r\n vrm.expressionManager?.setValue(VRMExpressionPresetName.Blink, 2 - blinkProgress)\r\n } else {\r\n vrm.expressionManager?.setValue(VRMExpressionPresetName.Blink, 0)\r\n blinkTimer.current = 0\r\n nextBlinkAt.current = Math.random() * 4 + 2\r\n }\r\n }\r\n\r\n // ── EMOTION BLENDING ──\r\n const lerpSpeed = 4 * dt\r\n const allKeys = new Set([\r\n ...Object.keys(currentExpressions.current),\r\n ...Object.keys(targetExpressions.current),\r\n ])\r\n\r\n for (const key of allKeys) {\r\n const current = currentExpressions.current[key] || 0\r\n const target = targetExpressions.current[key] || 0\r\n const next = THREE.MathUtils.lerp(current, target, lerpSpeed)\r\n currentExpressions.current[key] = next\r\n\r\n // Don't override blink during active blink animation\r\n if (key === VRMExpressionPresetName.Blink && blinkTimer.current >= nextBlinkAt.current) continue\r\n\r\n vrm.expressionManager?.setValue(key, next)\r\n }\r\n\r\n // ── SPEAKING MOUTH ──\r\n if (callState === 'speaking' && audioLevel > 0.01) {\r\n const mouthTarget = Math.min(audioLevel * 1.5, 0.8)\r\n const currentMouth = vrm.expressionManager?.getValue(VRMExpressionPresetName.Aa) || 0\r\n const smoothMouth = THREE.MathUtils.lerp(currentMouth, mouthTarget, 8 * dt)\r\n vrm.expressionManager?.setValue(VRMExpressionPresetName.Aa, smoothMouth)\r\n } else {\r\n const currentMouth = vrm.expressionManager?.getValue(VRMExpressionPresetName.Aa) || 0\r\n if (currentMouth > 0.01) {\r\n vrm.expressionManager?.setValue(VRMExpressionPresetName.Aa, currentMouth * 0.9)\r\n }\r\n }\r\n\r\n // ── HEAD MOVEMENT ──\r\n if (vrm.humanoid) {\r\n const head = vrm.humanoid.getNormalizedBoneNode('head')\r\n if (head) {\r\n const idleSwayX = Math.sin(breathPhase.current * 0.3) * 0.01\r\n const idleSwayZ = Math.cos(breathPhase.current * 0.2) * 0.005\r\n\r\n const targetX = headTarget.current.x + idleSwayX\r\n const targetY = headTarget.current.y\r\n const targetZ = headTarget.current.z + idleSwayZ\r\n\r\n head.rotation.x = THREE.MathUtils.lerp(head.rotation.x, targetX, 3 * dt)\r\n head.rotation.y = THREE.MathUtils.lerp(head.rotation.y, targetY, 3 * dt)\r\n head.rotation.z = THREE.MathUtils.lerp(head.rotation.z, targetZ, 3 * dt)\r\n }\r\n }\r\n\r\n // ── CALL STATE REACTIONS ──\r\n if (callState === 'listening' && vrm.humanoid) {\r\n const head = vrm.humanoid.getNormalizedBoneNode('head')\r\n if (head) {\r\n head.rotation.z = THREE.MathUtils.lerp(head.rotation.z, 0.03, 2 * dt)\r\n }\r\n }\r\n\r\n // Update VRM (required for expression changes to take effect)\r\n vrm.update(dt)\r\n })\r\n\r\n return null\r\n}\r\n\r\n// ═══════════════════════════════════════\r\n// AUTO CAMERA FRAMING\r\n// ═══════════════════════════════════════\r\n\r\nfunction CameraSetup() {\r\n const { camera } = useThree()\r\n useEffect(() => {\r\n // Default camera position — will be overridden by model loader for GLB\r\n // For VRM: fixed head-level framing\r\n camera.position.set(0, 0.1, 0.6)\r\n camera.lookAt(0, 0.1, 0)\r\n }, [camera])\r\n return null\r\n}\r\n\r\n// ═══════════════════════════════════════\r\n// MAIN AVATAR 3D COMPONENT (exported)\r\n// ═══════════════════════════════════════\r\n\r\nexport default function Avatar3D({\r\n modelUrl,\r\n emotion,\r\n callState,\r\n audioLevel,\r\n primaryColor,\r\n size,\r\n}: Avatar3DProps) {\r\n const glowIntensity = useMemo(() => {\r\n switch (callState) {\r\n case 'speaking': return 0.6\r\n case 'listening': return 0.4\r\n case 'thinking': return 0.3\r\n default: return 0.15\r\n }\r\n }, [callState])\r\n\r\n const isActive = callState === 'listening' || callState === 'speaking'\r\n\r\n return (\r\n <div className=\"relative flex flex-col items-center gap-4\">\r\n {/* Glow ring behind canvas */}\r\n <div\r\n className=\"relative rounded-full overflow-hidden\"\r\n style={{\r\n width: `${size}px`,\r\n height: `${size}px`,\r\n boxShadow: isActive\r\n ? `0 0 0 3px ${primaryColor}50, 0 0 30px ${primaryColor}${Math.floor(glowIntensity * 255).toString(16).padStart(2, '0')}, 0 0 60px ${primaryColor}20`\r\n : `0 0 0 2px ${primaryColor}25`,\r\n transition: 'box-shadow 0.5s ease',\r\n }}\r\n >\r\n <Canvas\r\n style={{ width: '100%', height: '100%', background: 'transparent' }}\r\n gl={{ alpha: true, antialias: true, preserveDrawingBuffer: false }}\r\n camera={{ fov: 30, near: 0.01, far: 10 }}\r\n dpr={[1, 2]}\r\n >\r\n <CameraSetup />\r\n\r\n {/* Lighting — soft studio setup */}\r\n {/* @ts-ignore R3F IntrinsicElements conflict */}\r\n <ambientLight intensity={0.6} />\r\n {/* @ts-ignore */}\r\n <directionalLight position={[1, 2, 3]} intensity={0.8} color=\"#ffffff\" />\r\n {/* @ts-ignore */}\r\n <directionalLight position={[-1, 1, -1]} intensity={0.3} color=\"#b4c6e7\" />\r\n\r\n {/* Rim light for depth — matches primary color */}\r\n {/* @ts-ignore */}\r\n <pointLight\r\n position={[0, 0.5, -0.5]}\r\n intensity={glowIntensity * 2}\r\n color={primaryColor}\r\n distance={3}\r\n />\r\n\r\n <VRMModel\r\n url={modelUrl}\r\n emotion={emotion}\r\n callState={callState}\r\n audioLevel={audioLevel}\r\n />\r\n </Canvas>\r\n </div>\r\n </div>\r\n )\r\n}\r\n"],"names":["EMOTION_MAP","default","expressions","VRMExpressionPresetName","Neutral","happy","Happy","angry","Angry","headRotation","sorry","Sad","confused","love","Relaxed","thinking","writing","wink","Blink","VRMModel","url","emotion","callState","audioLevel","vrmRef","useRef","glbSceneRef","mixerRef","baseRotationY","baseScale","blinkTimer","nextBlinkAt","breathPhase","currentExpressions","targetExpressions","headTarget","THREE","Euler","baseY","scene","camera","useThree","useEffect","loader","GLTFLoader","register","parser","VRMLoaderPlugin","load","gltf","vrm","userData","current","rotation","y","Math","PI","box","Box3","setFromObject","center","getCenter","Vector3","height","getSize","position","add","console","info","model","updateMatrixWorld","size","pivot","Group","sub","fov","headY","cameraZ","tan","set","lookAt","animations","length","mixer","AnimationMixer","forEach","clip","clipAction","play","error","remove","stopAllAction","config","useFrame","_","delta","dt","min","glb","update","breathOffset","sin","idleSway","pulse","targetScale","currentScale","scale","x","setScalar","MathUtils","lerp","abs","blinkProgress","expressionManager","setValue","random","lerpSpeed","allKeys","Set","Object","keys","key","target","next","mouthTarget","currentMouth","getValue","Aa","smoothMouth","humanoid","head","getNormalizedBoneNode","idleSwayX","idleSwayZ","cos","targetX","targetY","targetZ","z","CameraSetup","Avatar3D","modelUrl","primaryColor","glowIntensity","useMemo","jsx","className","children","style","width","boxShadow","floor","toString","padStart","transition","jsxs","Canvas","background","gl","alpha","antialias","preserveDrawingBuffer","near","far","dpr","intensity","color","distance"],"mappings":"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"}