"use client";
/* eslint-disable @next/next/no-img-element */
import dynamic from "next/dynamic";
import { Box, Download, Loader2, RotateCcw } from "lucide-react";
import { useMemo, useState } from "react";
import {
buildOpenClawArtifactProxyUrl,
isOpenClawInteractive3DArtifact,
type OpenClawArtifact,
} from "@/lib/openclaw-chat/artifacts";
const Model3DViewer = dynamic(
() =>
import("@/components/canvas/model3d/Model3DViewer").then(
(module) => module.Model3DViewer
),
{
ssr: false,
loading: () => (
),
}
);
function supportsWebGl() {
try {
const canvas = document.createElement("canvas");
return Boolean(
canvas.getContext("webgl2") ||
canvas.getContext("webgl") ||
canvas.getContext("experimental-webgl")
);
} catch {
return false;
}
}
export function OpenClawModel3DPreview({
artifacts,
chatId,
messageId,
}: {
artifacts: OpenClawArtifact[];
chatId: string;
messageId: string;
}) {
const modelIndex = useMemo(
() => artifacts.findIndex(isOpenClawInteractive3DArtifact),
[artifacts]
);
const model = modelIndex >= 0 ? artifacts[modelIndex] : null;
const fallbackRole = model?.viewer?.fallbackArtifactRole || "cad_preview";
const previewIndex = artifacts.findIndex(
(artifact) => artifact.artifactRole === fallbackRole
);
const [isActive, setIsActive] = useState(false);
const [webGlUnavailable, setWebGlUnavailable] = useState(false);
const [resetViewToken, setResetViewToken] = useState(0);
if (!model) return null;
const modelUrl = buildOpenClawArtifactProxyUrl({
chatId,
messageId,
artifactIndex: modelIndex,
});
const downloadUrl = buildOpenClawArtifactProxyUrl({
chatId,
messageId,
artifactIndex: modelIndex,
download: true,
});
const previewUrl =
previewIndex >= 0
? buildOpenClawArtifactProxyUrl({
chatId,
messageId,
artifactIndex: previewIndex,
})
: null;
const activateViewer = () => {
if (!supportsWebGl()) {
setWebGlUnavailable(true);
return;
}
setWebGlUnavailable(false);
setIsActive(true);
};
return (
{isActive ? (
) : (
{previewUrl ? (

) : (
)}
)}
{webGlUnavailable ? (
当前浏览器无法启动3D预览,已保留静态预览和模型下载。
) : null}
);
}