From 6dc705b3325a2740ec7c0bc8a0462d7425220b93 Mon Sep 17 00:00:00 2001 From: "Najjar\\NajjarV02" Date: Fri, 17 Apr 2026 15:19:54 +0400 Subject: [PATCH] feat: add AttireErrorBoundary component for improved error handling in attire model loading --- src/components/RobotModel.tsx | 41 ++++++++++++++++++++++++++++------- src/store/usePersonaStore.ts | 16 +++----------- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/components/RobotModel.tsx b/src/components/RobotModel.tsx index 43eb884..263f6d6 100644 --- a/src/components/RobotModel.tsx +++ b/src/components/RobotModel.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useRef, useMemo, useEffect, Suspense, useState } from 'react'; +import { useRef, useMemo, useEffect, Suspense, useState, Component, type ReactNode } from 'react'; import { useGLTF } from '@react-three/drei'; import { useFrame } from '@react-three/fiber'; import * as THREE from 'three'; @@ -24,6 +24,30 @@ function buildAttireGlbMap(personas: { id: string; modelPath?: string }[]): Reco return { ...STATIC_ATTIRE_GLB, ...dynamic }; // uploaded GLBs override static } +interface AttireErrorBoundaryProps { + children: ReactNode; + onError: () => void; +} +interface AttireErrorBoundaryState { hasError: boolean; } + +class AttireErrorBoundary extends Component { + constructor(props: AttireErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + static getDerivedStateFromError(): AttireErrorBoundaryState { + return { hasError: true }; + } + componentDidCatch(error: Error) { + console.warn('[AttireModel] Failed to load GLB, falling back to base robot:', error.message); + this.props.onError(); + } + render() { + if (this.state.hasError) return null; + return this.props.children; + } +} + function easeInOutCubic(t: number): number { return t < 0.5 ? 4 * t * t * t @@ -205,13 +229,14 @@ export function RobotModel({ onError }: RobotModelProps) { {attireGlbPath && ( - - - + { setDisplayedAttire('none'); setAttireReady(false); }}> + + + + )} ); diff --git a/src/store/usePersonaStore.ts b/src/store/usePersonaStore.ts index 72ae879..d728707 100644 --- a/src/store/usePersonaStore.ts +++ b/src/store/usePersonaStore.ts @@ -49,18 +49,6 @@ export const DEFAULT_PERSONAS: PersonaOption[] = [ description: 'Professional navy suit', colors: { torso: '#1e293b', legs: '#1e293b' }, }, - { - id: 'robot-doctor', - label: 'Robot Doctor', - description: 'Medical doctor attire', - colors: { torso: '#ffffff', legs: '#ffffff' }, - }, - { - id: 'security-guard', - label: 'Security Guard', - description: 'Security personnel uniform', - colors: { torso: '#1c1c1c', legs: '#1c1c1c' }, - }, ]; const STORAGE_KEY = 'lootah-personas'; @@ -138,11 +126,13 @@ export const personaStore = createStore((set, get) => ({ hydrate: () => { const stored = loadFromStorage(); if (stored && stored.length > 0) { + // Only re-inject truly built-in personas (those still in DEFAULT_PERSONAS) if missing. + // Dynamic/uploaded personas that were deleted via the dashboard must NOT be re-added. const storedIds = new Set(stored.map((s) => s.id)); const missing = DEFAULT_PERSONAS.filter((d) => !storedIds.has(d.id)); set({ personas: [...stored, ...missing], isHydrated: true }); } else { - set({ isHydrated: true }); + set({ personas: [...DEFAULT_PERSONAS], isHydrated: true }); } }, }));