diff --git a/src/features/studio-world/preview/StudioWorldPreview.tsx b/src/features/studio-world/preview/StudioWorldPreview.tsx index 130d492..fe51d1b 100644 --- a/src/features/studio-world/preview/StudioWorldPreview.tsx +++ b/src/features/studio-world/preview/StudioWorldPreview.tsx @@ -112,7 +112,7 @@ const SceneContents = ({ sceneDraft }: { sceneDraft: StudioWorldDraft }) => { type StudioWorldPreviewProps = { sceneDraft: StudioWorldDraft; referenceImage?: StudioSourceImageRecord | null; - project?: Pick | null; + project?: Pick | null; }; export function StudioWorldPreview({ @@ -120,6 +120,23 @@ export function StudioWorldPreview({ referenceImage = null, project = null, }: StudioWorldPreviewProps) { + const isRemoteAiProject = project?.provider === "meshy"; + const remoteReady = Boolean(project?.externalModel?.glbUrl); + const remoteThumbnailUrl = project?.externalModel?.thumbnailUrl ?? null; + const previewLabel = isRemoteAiProject + ? remoteReady + ? "Remote AI result available" + : "Remote AI task in progress" + : "Local Studio preview"; + const previewSubLabel = isRemoteAiProject + ? remoteReady + ? "Showing local fallback scene while provider GLB and thumbnail are ready." + : "Showing local fallback scene until the provider finishes." + : project?.mode === "image_avatar" + ? "Image-guided avatar proxy." + : project?.mode === "image_mesh" + ? "Image-guided mesh draft." + : "Local world draft."; return (

{sceneDraft.promptSummary}

+

{previewLabel}

{sceneDraft.assets.length} assets
+
+
+ {previewSubLabel} +
+
{referenceImage ? (
{referenceImage.fileName}
- {project?.mode === "image_avatar" ? "Image-guided avatar proxy." : "Reference image attached."} + {project?.mode === "image_avatar" + ? "Image-guided avatar proxy." + : project?.mode === "image_mesh" + ? "Image-guided mesh draft." + : "Reference image attached."} +
+
+ + ) : null} + {remoteThumbnailUrl ? ( +
+ Remote AI thumbnail +
+
+ Provider result +
+
+ Remote thumbnail returned by the AI provider.
diff --git a/src/features/studio-world/screens/StudioWorldScreen.tsx b/src/features/studio-world/screens/StudioWorldScreen.tsx index 4b24e5f..5af8e8f 100644 --- a/src/features/studio-world/screens/StudioWorldScreen.tsx +++ b/src/features/studio-world/screens/StudioWorldScreen.tsx @@ -63,6 +63,23 @@ type StudioWorldResponse = { error?: string; }; +const downloadFileFromUrl = async (params: { + url: string; + filename: string; +}) => { + const response = await fetch(params.url); + if (!response.ok) { + throw new Error("Failed to download provider GLB."); + } + const blob = await response.blob(); + const objectUrl = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = objectUrl; + link.download = params.filename; + link.click(); + URL.revokeObjectURL(objectUrl); +}; + const formatTimestamp = (value: string) => { const parsed = Date.parse(value); if (!Number.isFinite(parsed)) return value; @@ -262,6 +279,38 @@ export function StudioWorldScreen() { } }; + const handleSyncProject = async (projectId: string) => { + setBusy(true); + setStatusLine("Syncing provider task status."); + try { + const response = await fetch( + `/api/studio-world?action=task-status&projectId=${encodeURIComponent(projectId)}`, + { cache: "no-store" }, + ); + const body = (await response.json()) as StudioWorldResponse; + if (!response.ok || !body.project) { + throw new Error(body.error || "Failed to sync provider task."); + } + setProjects((current) => + current.map((entry) => (entry.id === body.project!.id ? body.project! : entry)), + ); + setSelectedProjectId(body.project.id); + setError(null); + if (body.providerTask?.status === "SUCCEEDED") { + setStatusLine("Provider task synced. Remote GLB is ready."); + } else if (body.providerTask?.status === "FAILED" || body.providerTask?.status === "CANCELED") { + setStatusLine(body.providerTask.taskErrorMessage || "Provider task failed."); + } else { + setStatusLine(`Provider task is ${body.providerTask?.status?.toLowerCase() ?? "in progress"}.`); + } + } catch (syncError) { + setError(syncError instanceof Error ? syncError.message : "Failed to sync provider task."); + setStatusLine(null); + } finally { + setBusy(false); + } + }; + const handleDelete = async (projectId: string) => { setBusy(true); setStatusLine("Deleting studio draft."); @@ -324,6 +373,29 @@ export function StudioWorldScreen() { } }; + const handleExportProviderGlb = async (project: StudioProjectRecord) => { + const glbUrl = project.externalModel?.glbUrl?.trim() ?? ""; + if (!glbUrl) { + setError("Provider GLB is not ready yet."); + return; + } + setBusy(true); + setStatusLine("Downloading provider GLB."); + try { + await downloadFileFromUrl({ + url: glbUrl, + filename: `${project.name.trim().replace(/\s+/g, "-").toLowerCase() || "studio-provider"}.glb`, + }); + setError(null); + setStatusLine("Provider GLB downloaded."); + } catch (downloadError) { + setError(downloadError instanceof Error ? downloadError.message : "Failed to download provider GLB."); + setStatusLine(null); + } finally { + setBusy(false); + } + }; + const handleApplyToOffice = async (projectId: string) => { setBusy(true); setStatusLine("Publishing generated layout to Claw3D office."); @@ -596,7 +668,11 @@ export function StudioWorldScreen() {
{selectedProject ? (
- +
@@ -616,9 +692,10 @@ export function StudioWorldScreen() {
Direct GLB download.
+
Provider GLB download when remote AI finishes.
GLB manifest download.
Publish to Claw3D office layout.
-
Future provider seam for remote model workers.
+
Manual provider task sync for remote AI jobs.
@@ -704,6 +781,18 @@ export function StudioWorldScreen() { {project.externalModel.errorMessage ? ` ${project.externalModel.errorMessage}` : ""}
) : null} + {project.externalModel?.thumbnailUrl ? ( +
+ {`${project.name} +
+ ) : null} {project.sourceImages[0] ? (
) : null}
+ {project.externalModel?.taskId ? ( + + ) : null} + {project.externalModel?.glbUrl ? ( + + ) : null}