"use client"; import type { DragEvent } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { AssistantRuntimeProvider, ComposerPrimitive, MessagePartPrimitive, MessagePrimitive, ThreadPrimitive, useMessage, type DataMessagePartProps, type ToolCallMessagePartProps, } from "@assistant-ui/react"; import { AssistantChatTransport, useChatRuntime } from "@assistant-ui/react-ai-sdk"; import type { UIMessage } from "ai"; import { CadViewerPreview } from "@/components/cad-viewer-preview"; import { AiSelectionMode } from "@/lib/cad-edit-tools"; import { Bot, Box, Check, CircleAlert, FileImage, FileUp, Loader2, MessageSquare, Paperclip, Send, Settings2, Sparkles, Square, Wrench, } from "lucide-react"; type Attachment = { id: string; taskId: string; name: string; kind: "image" | "step" | "source" | "manifest" | "other"; path: string; size: number; sha256: string; mime: string; artifactUrl?: string; previewPath?: string; previewUrl?: string; viewerAssetPath?: string; viewerAssetUrl?: string; previewError?: string; }; type ViewerContext = { id: string; type: string; payload: Record; createdAt: string; }; type CadResult = { taskId: string; sourcePath?: string; artifactPath?: string; artifactUrl?: string; previewPath?: string; previewUrl?: string; viewerAssetPath?: string; viewerAssetUrl?: string; summary?: string; }; type CadProgress = { step: string; label: string; status: "pending" | "running" | "success" | "error"; message?: string; }; type CadError = { stage?: string; message: string; }; type PublicConfig = { defaultProvider: string; defaultModel: string; providers: Record; }>; uploads: { maxImageMB: number; maxStepMB: number; }; }; const initialMessages: UIMessage[] = [{ id: "welcome", role: "assistant", parts: [{ type: "text", text: "CAD Agent Studio 已就绪。上传图片、STEP 或其他文件,或直接描述要生成/修改的模型。", }], }]; const DEEPSEEK_PROVIDER_ID = "deepseek"; function uniqueId(prefix: string) { return `${prefix}_${Math.random().toString(36).slice(2, 10)}`; } function formatBytes(size: number) { if (size < 1024) return `${size} B`; if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`; return `${(size / 1024 / 1024).toFixed(1)} MB`; } function isStepFile(file: File) { return /\.(step|stp)$/i.test(file.name); } function extractCadResult(text: string) { const match = /::cad-result\{base64="([^"]+)"\}/.exec(text); if (!match) return null; try { return JSON.parse(atob(match[1])) as CadResult; } catch { return null; } } function setIfChanged(setter: (value: string | ((current: string) => string)) => void, nextValue = "") { setter((current) => (current === nextValue ? current : nextValue)); } function UploadedAttachmentCard({ attachment }: { attachment: Attachment }) { return (
{attachment.kind === "image" ? : } {attachment.name} {attachment.kind === "step" ? ( 重建参考 ) : null} {formatBytes(attachment.size)}
); } function CadProgressCard({ data }: DataMessagePartProps) { const icon = data.status === "running" ? : data.status === "success" ? : data.status === "error" ? : ; return (
{icon} {data.label} {data.status}
{data.message ?
{data.message}
: null}
); } function CadArtifactCard({ data }: DataMessagePartProps) { return (
CAD 结果
{data.taskId}
{data.sourcePath ?
{data.sourcePath}
: null} {data.artifactPath ?
{data.artifactPath}
: null}
{data.artifactUrl ? ( 下载 STEP ) : null}
); } function CadErrorCard({ data }: DataMessagePartProps) { return (
{data.stage ? `${data.stage} 失败` : "CAD 操作失败"}
{data.message}
); } function CadToolCallCard(props: ToolCallMessagePartProps) { const status = String(props.status?.type || props.status || ""); return (
{status.includes("running") || status.includes("input-streaming") ? : } {props.toolName} {status || "tool call"}
); } function DataFallback({ name, data }: DataMessagePartProps) { return (
{name}: {JSON.stringify(data).slice(0, 500)}
); } function MessageText() { return ( (

), data: { by_name: { "cad-progress": CadProgressCard, "cad-result": CadArtifactCard, "cad-error": CadErrorCard, }, Fallback: DataFallback, }, tools: { Fallback: CadToolCallCard, }, }} /> ); } function UserMessage() { return (
You
); } function AssistantMessage() { return (
Agent
Agent 请求失败,请检查服务端日志和模型配置。
); } function AnyMessage() { const message = useMessage(); if (message.role === "user") return ; if (message.role === "assistant") return ; return null; } export function AgentStudio() { const [config, setConfig] = useState(null); const [provider, setProvider] = useState(DEEPSEEK_PROVIDER_ID); const [model, setModel] = useState(""); const [attachments, setAttachments] = useState([]); const [pendingViewerContexts, setPendingViewerContexts] = useState([]); const [currentTaskId, setCurrentTaskId] = useState(""); const [previewUrl, setPreviewUrl] = useState(""); const [artifactUrl, setArtifactUrl] = useState(""); const [artifactPath, setArtifactPath] = useState(""); const [viewerAssetUrl, setViewerAssetUrl] = useState(""); const [activeEditToolId, setActiveEditToolId] = useState(""); const [aiSelectionMode, setAiSelectionMode] = useState("point"); const [uploading, setUploading] = useState(false); const [draggingUpload, setDraggingUpload] = useState(false); const [error, setError] = useState(""); const dragDepthRef = useRef(0); const fileInputRef = useRef(null); const applyCadResult = useCallback((result: CadResult) => { if (!result.taskId) return; setIfChanged(setCurrentTaskId, result.taskId); setIfChanged(setArtifactPath, result.artifactPath || ""); setIfChanged(setArtifactUrl, result.artifactUrl || ""); setIfChanged(setPreviewUrl, result.previewUrl || ""); setIfChanged(setViewerAssetUrl, result.viewerAssetUrl || ""); setActiveEditToolId(""); setAiSelectionMode("point"); }, []); const handleData = useCallback((dataPart: { type?: string; data?: unknown }) => { if (dataPart.type === "data-cad-result") { applyCadResult(dataPart.data as CadResult); setPendingViewerContexts([]); return; } if (dataPart.type === "data-cad-error") { const data = dataPart.data as CadError; setError(data.message || "CAD 生成失败"); } }, [applyCadResult]); const transport = useMemo(() => new AssistantChatTransport({ api: "/api/chat", body: { provider: DEEPSEEK_PROVIDER_ID, model, attachments, viewerContext: pendingViewerContexts, selectedTaskId: currentTaskId, }, }), [attachments, currentTaskId, model, pendingViewerContexts]); const runtime = useChatRuntime({ messages: initialMessages, transport, onData: handleData, onError: (nextError) => { setError(nextError instanceof Error ? nextError.message : "Agent 回复失败"); }, }); useEffect(() => { void fetch("/api/config") .then((response) => response.json()) .then((nextConfig: PublicConfig) => { setConfig(nextConfig); setProvider(DEEPSEEK_PROVIDER_ID); const deepseekProvider = nextConfig.providers[DEEPSEEK_PROVIDER_ID]; setModel(deepseekProvider?.models?.default || ""); }) .catch((nextError) => { setError(nextError instanceof Error ? nextError.message : "配置加载失败"); }); }, []); const providerModels = useMemo(() => { const models = config?.providers?.[DEEPSEEK_PROVIDER_ID]?.models || {}; return Object.entries(models); }, [config]); const pushViewerContext = useCallback((type: string, payload: Record) => { if (!currentTaskId) { setError("请先生成或上传一个模型,再使用右侧工具栏。"); return; } const context: ViewerContext = { id: uniqueId("viewer"), type, createdAt: new Date().toISOString(), payload, }; setPendingViewerContexts((current) => { const nextCurrent = type === "cadViewer.selection" ? current.filter((item) => item.type !== "cadViewer.selection") : current; return [context, ...nextCurrent].slice(0, 8); }); }, [currentTaskId]); const handleEditIntent = useCallback((intent: Record) => { if (!currentTaskId) return; pushViewerContext("cadViewer.editIntent", { ...intent, viewerState: { viewerAssetUrl, renderer: "cad-viewer.ported", }, }); }, [currentTaskId, pushViewerContext, viewerAssetUrl]); const handleGeometrySelection = useCallback((selectionIntent: Record) => { if (!currentTaskId) return; pushViewerContext("cadViewer.selection", { ...selectionIntent, viewerState: { viewerAssetUrl, renderer: "cad-viewer.ported", }, }); }, [currentTaskId, pushViewerContext, viewerAssetUrl]); const handleUpload = useCallback(async (files: FileList | null) => { if (!files?.length) return; setUploading(true); setError(""); try { for (const file of Array.from(files)) { const form = new FormData(); form.append("file", file); const stepUpload = isStepFile(file); if (currentTaskId && !stepUpload) { form.append("taskId", currentTaskId); } const response = await fetch("/api/upload", { method: "POST", body: form, }); const attachment = await response.json(); if (!response.ok) { throw new Error(attachment.error || `${file.name} 上传失败`); } setAttachments((current) => [attachment, ...current]); if (attachment.kind === "step") { continue; } setCurrentTaskId(attachment.taskId); if (attachment.artifactUrl) { setArtifactUrl(attachment.artifactUrl); setArtifactPath(attachment.path); } if (attachment.previewUrl) { setPreviewUrl(attachment.previewUrl); } if (attachment.viewerAssetUrl) { setViewerAssetUrl(attachment.viewerAssetUrl); } if (attachment.previewError) { setError(`STEP 已上传,但预览网格生成失败:${attachment.previewError}`); } } } catch (nextError) { setError(nextError instanceof Error ? nextError.message : "上传失败"); } finally { setUploading(false); if (fileInputRef.current) { fileInputRef.current.value = ""; } } }, [currentTaskId]); const handleDragEnter = useCallback((event: DragEvent) => { event.preventDefault(); event.stopPropagation(); dragDepthRef.current += 1; if (event.dataTransfer.types.includes("Files")) { setDraggingUpload(true); } }, []); const handleDragOver = useCallback((event: DragEvent) => { event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = "copy"; if (event.dataTransfer.types.includes("Files")) { setDraggingUpload(true); } }, []); const handleDragLeave = useCallback((event: DragEvent) => { event.preventDefault(); event.stopPropagation(); dragDepthRef.current = Math.max(0, dragDepthRef.current - 1); if (dragDepthRef.current === 0) { setDraggingUpload(false); } }, []); const handleDrop = useCallback((event: DragEvent) => { event.preventDefault(); event.stopPropagation(); dragDepthRef.current = 0; setDraggingUpload(false); void handleUpload(event.dataTransfer.files); }, [handleUpload]); return (
{draggingUpload ? (
拖放文件上传
支持图片、STEP/STP 和其他参考文件
) : null}
void handleUpload(event.target.files)} />
{error ? (
{error}
) : null} {attachments.length ? (
{attachments.slice(0, 5).map((attachment) => ( ))}
) : null}
Enter 发送,Shift + Enter 换行
发送
{viewerAssetUrl ? ( ) : (
3D 预览等待模型
上传文件会进入 Agent 上下文。生成、重建或修改完成后会切换到真实输出结果。
)} {artifactUrl ? ( STEP ) : null}
); }