feat: add Robot Doctor and Security Guard personas with pricing and model paths
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions

This commit is contained in:
Najjar\NajjarV02
2026-04-17 10:10:54 +04:00
parent f53b2d3cb8
commit 0793a650fb
8 changed files with 62 additions and 6 deletions
+32 -3
View File
@@ -37,9 +37,36 @@ export default function AdminPage() {
const [rowGlbError, setRowGlbError] = useState<Record<string, string>>({});
const [syncingGlbs, setSyncingGlbs] = useState(false);
const [syncResult, setSyncResult] = useState('');
// Local state for immediate badge update after upload (bypasses any store re-render delay)
const [uploadedPaths, setUploadedPaths] = useState<Record<string, string>>({});
useEffect(() => { pricingStore.getState().hydrate(); }, []);
// Auto-sync GLB files from server after hydration — ensures modelPaths are always
// populated regardless of which browser/session last uploaded the files.
useEffect(() => {
if (!isPricingHydrated) return;
fetch('/api/admin/list-models/')
.then((r) => r.json())
.then((data) => {
const models: { id: string; modelPath: string }[] = data.models ?? [];
models.forEach(({ id, modelPath }) => {
const item = pricingStore.getState().items.find((i) => i.id === id);
// Only link if the item exists and has no modelPath yet (don't overwrite versioned upload URLs)
if (item && !item.modelPath) {
pricingStore.getState().updateItem(id, { modelPath });
const existing = personaStore.getState().personas.find((p) => p.id === id);
if (existing) {
personaStore.getState().updatePersona(id, { modelPath });
} else {
personaStore.getState().addPersona({ id, label: item.label, description: item.label, colors: { torso: '#3b82f6', legs: '#3b82f6' }, modelPath });
}
}
});
})
.catch(() => {}); // silent — non-critical
}, [isPricingHydrated]);
useEffect(() => {
const priceMap: Record<string, number> = {};
const labelMap: Record<string, string> = {};
@@ -93,6 +120,8 @@ export default function AdminPage() {
modelPath,
});
}
// Immediately reflect the new path in the badge regardless of store re-render timing
setUploadedPaths((p) => ({ ...p, [itemId]: modelPath }));
setRowGlbFiles((p) => ({ ...p, [itemId]: null }));
} catch (err) {
setRowGlbError((p) => ({ ...p, [itemId]: err instanceof Error ? err.message : 'Upload failed' }));
@@ -432,12 +461,12 @@ export default function AdminPage() {
{/* 3D Model column */}
<div style={{ display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
{item.modelPath ? (
{(uploadedPaths[item.id] || item.modelPath) ? (
<span
title={item.modelPath}
title={uploadedPaths[item.id] || item.modelPath}
style={{ display: 'inline-flex', alignItems: 'center', gap: '0.25rem', fontSize: '0.7rem', color: '#16a34a', background: 'rgba(22,163,74,0.08)', border: '1px solid rgba(22,163,74,0.2)', borderRadius: '0.375rem', padding: '0.2rem 0.45rem', whiteSpace: 'nowrap', overflow: 'hidden', maxWidth: '100%' }}
>
✓ {item.modelPath.split('/').pop()}
✓ {(uploadedPaths[item.id] || item.modelPath)!.split('/').pop()?.split('?')[0]}
</span>
) : null}
<label
+3 -1
View File
@@ -51,5 +51,7 @@ export async function POST(request: Request) {
const buffer = Buffer.from(await file.arrayBuffer());
await writeFile(destPath, buffer);
return NextResponse.json({ modelPath: `/models/${safeId}.glb` });
// Append a version timestamp so useGLTF cache is busted on replacement uploads
const version = Date.now();
return NextResponse.json({ modelPath: `/models/${safeId}.glb?v=${version}` });
}