mirror of
https://github.com/iamlukethedev/Claw3D.git
synced 2026-07-31 03:52:15 +00:00
Polish Meshy-backed studio results
Co-authored-by: Luke The Dev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
co-authored by
Luke The Dev
parent
fa2e15edb8
commit
dcfc93c285
@@ -112,7 +112,7 @@ const SceneContents = ({ sceneDraft }: { sceneDraft: StudioWorldDraft }) => {
|
||||
type StudioWorldPreviewProps = {
|
||||
sceneDraft: StudioWorldDraft;
|
||||
referenceImage?: StudioSourceImageRecord | null;
|
||||
project?: Pick<StudioProjectRecord, "mode"> | null;
|
||||
project?: Pick<StudioProjectRecord, "mode" | "provider" | "externalModel"> | 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 (
|
||||
<div className="relative h-full min-h-[360px] w-full overflow-hidden rounded-2xl border border-border/60 bg-black/70">
|
||||
<Canvas
|
||||
@@ -137,11 +154,17 @@ export function StudioWorldPreview({
|
||||
Claw3D Studio Preview
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-white/90">{sceneDraft.promptSummary}</p>
|
||||
<p className="mt-1 text-[11px] text-white/65">{previewLabel}</p>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/15 bg-black/35 px-3 py-1 font-mono text-[10px] uppercase tracking-[0.16em] text-white/75">
|
||||
{sceneDraft.assets.length} assets
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-x-0 top-16 px-4">
|
||||
<div className="inline-flex rounded-full border border-white/10 bg-black/35 px-3 py-1 font-mono text-[10px] uppercase tracking-[0.14em] text-white/70">
|
||||
{previewSubLabel}
|
||||
</div>
|
||||
</div>
|
||||
{referenceImage ? (
|
||||
<div className="pointer-events-none absolute bottom-4 left-4 w-36 overflow-hidden rounded-2xl border border-white/15 bg-black/45 shadow-2xl backdrop-blur">
|
||||
<Image
|
||||
@@ -158,7 +181,31 @@ export function StudioWorldPreview({
|
||||
</div>
|
||||
<div className="mt-1 truncate text-xs text-white/85">{referenceImage.fileName}</div>
|
||||
<div className="mt-1 text-[11px] text-white/60">
|
||||
{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."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{remoteThumbnailUrl ? (
|
||||
<div className="pointer-events-none absolute bottom-4 right-4 w-40 overflow-hidden rounded-2xl border border-white/15 bg-black/45 shadow-2xl backdrop-blur">
|
||||
<Image
|
||||
src={remoteThumbnailUrl}
|
||||
alt="Remote AI thumbnail"
|
||||
width={160}
|
||||
height={160}
|
||||
className="h-28 w-full object-cover"
|
||||
unoptimized
|
||||
/>
|
||||
<div className="px-3 py-2">
|
||||
<div className="font-mono text-[10px] uppercase tracking-[0.16em] text-cyan-100/80">
|
||||
Provider result
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] text-white/60">
|
||||
Remote thumbnail returned by the AI provider.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
<section className="ui-panel ui-depth-workspace min-h-0 overflow-hidden p-3">
|
||||
{selectedProject ? (
|
||||
<div className="flex h-full min-h-0 flex-col gap-3">
|
||||
<StudioWorldPreview sceneDraft={selectedProject.sceneDraft} />
|
||||
<StudioWorldPreview
|
||||
sceneDraft={selectedProject.sceneDraft}
|
||||
referenceImage={selectedProject.sourceImages[0] ?? null}
|
||||
project={selectedProject}
|
||||
/>
|
||||
<div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_280px]">
|
||||
<div className="ui-card max-h-52 overflow-auto p-4">
|
||||
<div className="font-mono text-[10px] uppercase tracking-[0.16em] text-muted-foreground">
|
||||
@@ -616,9 +692,10 @@ export function StudioWorldScreen() {
|
||||
</div>
|
||||
<div className="mt-3 space-y-2 text-sm text-muted-foreground">
|
||||
<div className="rounded-lg border border-border/50 bg-surface-1/50 px-3 py-2">Direct GLB download.</div>
|
||||
<div className="rounded-lg border border-border/50 bg-surface-1/50 px-3 py-2">Provider GLB download when remote AI finishes.</div>
|
||||
<div className="rounded-lg border border-border/50 bg-surface-1/50 px-3 py-2">GLB manifest download.</div>
|
||||
<div className="rounded-lg border border-border/50 bg-surface-1/50 px-3 py-2">Publish to Claw3D office layout.</div>
|
||||
<div className="rounded-lg border border-border/50 bg-surface-1/50 px-3 py-2">Future provider seam for remote model workers.</div>
|
||||
<div className="rounded-lg border border-border/50 bg-surface-1/50 px-3 py-2">Manual provider task sync for remote AI jobs.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -704,6 +781,18 @@ export function StudioWorldScreen() {
|
||||
{project.externalModel.errorMessage ? ` ${project.externalModel.errorMessage}` : ""}
|
||||
</div>
|
||||
) : null}
|
||||
{project.externalModel?.thumbnailUrl ? (
|
||||
<div className="mt-3 overflow-hidden rounded-xl border border-border/60 bg-surface-1/35">
|
||||
<Image
|
||||
src={project.externalModel.thumbnailUrl}
|
||||
alt={`${project.name} provider thumbnail`}
|
||||
width={320}
|
||||
height={180}
|
||||
className="h-32 w-full object-cover"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{project.sourceImages[0] ? (
|
||||
<div className="mt-3 flex items-center gap-3 rounded-xl border border-border/60 bg-surface-1/35 p-2">
|
||||
<Image
|
||||
@@ -723,6 +812,16 @@ export function StudioWorldScreen() {
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{project.externalModel?.taskId ? (
|
||||
<button
|
||||
type="button"
|
||||
className="ui-btn-secondary px-3 py-1.5 text-xs"
|
||||
onClick={() => void handleSyncProject(project.id)}
|
||||
disabled={busy}
|
||||
>
|
||||
Sync now
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="ui-btn-secondary px-3 py-1.5 text-xs"
|
||||
@@ -731,6 +830,16 @@ export function StudioWorldScreen() {
|
||||
>
|
||||
Export GLB
|
||||
</button>
|
||||
{project.externalModel?.glbUrl ? (
|
||||
<button
|
||||
type="button"
|
||||
className="ui-btn-secondary px-3 py-1.5 text-xs"
|
||||
onClick={() => void handleExportProviderGlb(project)}
|
||||
disabled={busy}
|
||||
>
|
||||
Download provider GLB
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="ui-btn-secondary px-3 py-1.5 text-xs"
|
||||
|
||||
Reference in New Issue
Block a user