feat: layout hamburger, media browser filters+previews, billing fixes
- Layout: mobile hamburger menu + overlay backdrop + close button; content area always full-width - Media browser: filter chips (default still+turntable); advanced toggle for GLB/STL; thumbnail_url previews for non-image types; video hover-play for turntable - Backend: asset_types multi-filter, thumbnail_url in MediaAssetOut, download proxy endpoint for MinIO/local files - Admin: "Import Existing Media" button → POST /api/admin/import-media-assets - Billing: fix invoice create 500 (MissingGreenlet — use selectinload after commit); PDF download uses axios blob instead of bare <a href> (auth header missing); fix storage.upload() accepting str|Path - SSE task logs: task_logs.py core + router, LiveRenderLog component - CadPreview: fix infinite loop when no gltf_geometry assets; loading screen before ThreeDViewer render - render-worker: add trimesh layer to Dockerfile Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,24 +1,30 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useQuery, useMutation } from '@tanstack/react-query'
|
||||
import { Box, Loader2, X } from 'lucide-react'
|
||||
import ThreeDViewer from '../components/cad/ThreeDViewer'
|
||||
import { getMediaAssets } from '../api/media'
|
||||
import { generateGltfGeometry } from '../api/cad'
|
||||
|
||||
/**
|
||||
* Route: /cad/:id
|
||||
*
|
||||
* Full-screen 3D viewer for a CAD file.
|
||||
* Passes production GLB URL if a gltf_geometry MediaAsset exists for this CAD file.
|
||||
* If no geometry GLB exists yet, offers to generate one on demand.
|
||||
*/
|
||||
export default function CadPreviewPage() {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
const [generating, setGenerating] = useState(false)
|
||||
|
||||
// Load any geometry GLB that was generated for this CAD file
|
||||
const { data: gltfAssets } = useQuery({
|
||||
// Poll every 3s while generating so it appears automatically
|
||||
const { data: gltfAssets, isLoading: gltfLoading } = useQuery({
|
||||
queryKey: ['media-assets', id, 'gltf_geometry'],
|
||||
queryFn: () => getMediaAssets({ cad_file_id: id!, asset_type: 'gltf_geometry' }),
|
||||
enabled: !!id,
|
||||
staleTime: 30_000,
|
||||
staleTime: 5_000,
|
||||
refetchInterval: generating ? 3_000 : false,
|
||||
})
|
||||
|
||||
// Load production GLB if available
|
||||
@@ -37,6 +43,20 @@ export default function CadPreviewPage() {
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const generateMutation = useMutation({
|
||||
mutationFn: () => generateGltfGeometry(id!),
|
||||
onSuccess: () => {
|
||||
setGenerating(true)
|
||||
},
|
||||
})
|
||||
|
||||
// Stop polling once asset appears
|
||||
useEffect(() => {
|
||||
if (generating && gltfAssets && gltfAssets.length > 0) {
|
||||
setGenerating(false)
|
||||
}
|
||||
}, [generating, gltfAssets])
|
||||
|
||||
if (!id) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full text-content-muted p-8">
|
||||
@@ -49,6 +69,61 @@ export default function CadPreviewPage() {
|
||||
const latestProduction = productionAssets?.[0]
|
||||
const latestBlend = blendAssets?.[0]
|
||||
|
||||
// While checking for assets, show a neutral loading screen (don't attempt to render ThreeDViewer)
|
||||
if (gltfLoading) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-gray-950 gap-3">
|
||||
<Loader2 size={36} className="animate-spin text-gray-400" />
|
||||
<p className="text-gray-400 text-sm">Checking for 3D model…</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// No GLB available yet — show generate prompt
|
||||
if (!latestGltf) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex flex-col bg-gray-950">
|
||||
<div className="flex items-center justify-between px-5 py-3 bg-gray-900 border-b border-gray-800">
|
||||
<span className="text-white font-semibold tracking-wide">3D Viewer</span>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="p-1.5 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-4 text-center px-8">
|
||||
<Box size={48} className="text-gray-600" />
|
||||
<p className="text-white text-lg font-semibold">No 3D model available yet</p>
|
||||
<p className="text-gray-400 text-sm max-w-sm">
|
||||
Generate a GLB file from the STEP cache to enable the 3D viewer.
|
||||
The STL cache must exist (process the STEP file first).
|
||||
</p>
|
||||
{generating ? (
|
||||
<div className="flex items-center gap-2 text-gray-300 text-sm">
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
Generating… checking every 3s
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => generateMutation.mutate()}
|
||||
disabled={generateMutation.isPending}
|
||||
className="px-5 py-2 rounded-md bg-accent hover:bg-accent-hover disabled:opacity-40 disabled:cursor-not-allowed text-white text-sm font-medium transition-colors flex items-center gap-2"
|
||||
>
|
||||
{generateMutation.isPending && <Loader2 size={14} className="animate-spin" />}
|
||||
Generate 3D Model
|
||||
</button>
|
||||
)}
|
||||
{generateMutation.isError && (
|
||||
<p className="text-red-400 text-sm">
|
||||
Failed to start generation. Check that the STL cache exists.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ThreeDViewer
|
||||
cadFileId={id}
|
||||
|
||||
Reference in New Issue
Block a user