refactor: streamline AI-CAD into single-screen workspace
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-06 21:15:41 +08:00
parent 9d222f0d42
commit 12bc8b1668

View File

@ -1,11 +1,11 @@
"use client";
import {
AlertCircle,
ArrowUpRight,
Bot,
Box,
BrainCircuit,
Camera,
CheckCircle2,
ChevronDown,
ChevronLeft,
@ -20,6 +20,7 @@ import {
Loader2,
MoreHorizontal,
Orbit,
Paperclip,
Plus,
RotateCcw,
Send,
@ -476,22 +477,6 @@ function getStatusBadgeClass(status: Model3DTaskStatus) {
}
}
function getMessageStateClass(state?: ChatMessageState) {
switch (state) {
case "success":
return "border-emerald-200 bg-emerald-50/70";
case "failed":
return "border-rose-200 bg-rose-50/80";
case "running":
return "border-sky-200 bg-sky-50/80";
case "queued":
case "thinking":
return "border-[#ddd7ca] bg-[#f5f1e8]";
default:
return "border-[#ddd7ca] bg-[#f5f1e8]";
}
}
function getMessageStateLabel(state?: ChatMessageState) {
switch (state) {
case "thinking":
@ -547,17 +532,6 @@ function getProviderLabel(provider: CadGenerationProvider) {
return provider === "kittycad-cad" ? "KittyCAD CAD" : "Tripo3D";
}
function getProviderSourceLabel(source?: AiCadProviderStatus["source"] | null) {
switch (source) {
case "provider":
return "系统设置";
case "env":
return "环境变量";
default:
return "未配置";
}
}
function readNullableText(value: unknown) {
return typeof value === "string" && value.trim() ? value : null;
}
@ -2260,8 +2234,7 @@ export function AiCadWorkbench() {
);
const [kittycadProviderStatus, setKittycadProviderStatus] =
useState<AiCadProviderStatus | null>(null);
const [isLoadingKittycadProviderStatus, setIsLoadingKittycadProviderStatus] =
useState(true);
const [, setIsLoadingKittycadProviderStatus] = useState(true);
const [viewerSettings, setViewerSettings] = useState<Model3DViewerSettings>(
DEFAULT_VIEWER_SETTINGS
);
@ -2587,7 +2560,6 @@ export function AiCadWorkbench() {
canIterateKittyCadVersion(activeVersion)
? activeVersion
: null;
const willSubmitAsIteration = Boolean(iterationBaseVersion);
const activeDownloadEntries = buildDownloadEntries(activeVersion);
const primaryDownloadEntry = activeDownloadEntries[0] ?? null;
const activeCodeEntry =
@ -2612,7 +2584,6 @@ export function AiCadWorkbench() {
const activeVersionStructuredDirectives = activeVersion
? extractStructuredPromptDirectives(activeVersion.prompt)
: [];
const promptStructuredDirectives = extractStructuredPromptDirectives(prompt);
const versionCompareNotes = buildVersionCompareNotes({
activeVersion,
parentVersion: activeParentVersion,
@ -2733,6 +2704,8 @@ export function AiCadWorkbench() {
workspaces.find((workspace) => workspace.id === activeWorkspaceId) ?? null;
const activeWorkspaceName = activeWorkspace?.name || "AI-CAD 工作台";
const normalizedWorkspaceName = normalizeWorkspaceTitle(activeWorkspaceName);
const shouldForceTreeCollapsed =
!activeVersion && activeLeftPanelView === "tree";
const isWorkspaceActionDisabled =
isSubmitting || isCreatingWorkspace || isDeletingWorkspace;
const canDeleteActiveWorkspace =
@ -2747,11 +2720,6 @@ export function AiCadWorkbench() {
!prompt.trim() ||
isSubmitting ||
(generationProvider === "kittycad-cad" && !isKittycadConfigured);
const promptCharacterCount = prompt.trim().length;
const promptLineCount = prompt.trim()
? prompt.trim().split(NEWLINE_SPLIT_RE).length
: 0;
const serializedWorkbenchSnapshot = serializeWorkbenchState({
prompt,
generationProvider,
@ -3256,20 +3224,6 @@ export function AiCadWorkbench() {
}));
}
function handleInsertStructuredDirective(
directiveKey: StructuredPromptDirectiveKey
) {
setPrompt((current) =>
upsertStructuredPromptDirective(current, directiveKey, activeVersion)
);
}
function handleInsertStructuredTemplate() {
setPrompt((current) =>
buildStructuredPromptTemplate(current, activeVersion)
);
}
async function handleCopyCodeContent(code: string) {
if (
typeof window === "undefined" ||
@ -3604,60 +3558,9 @@ export function AiCadWorkbench() {
</div>
</div>
<div className="border-[#e6e0d3] border-t px-4 py-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
<button
className="inline-flex items-center gap-2 rounded-xl border border-[#d8d2c4] bg-white px-3 py-1.5 font-medium text-[#201c16] text-[13px] transition-all duration-150 ease-out hover:bg-[#f4f1e8]"
type="button"
>
<WandSparkles className="h-4 w-4 text-[#32d67d]" />
Start Design
</button>
<Badge className="border-[#ddd7ca] bg-white text-[#5f594d]">
{generationProvider === "kittycad-cad"
? "KittyCAD / Zoo CAD"
: "Tripo3D"}
</Badge>
<Badge className="border-[#ddd7ca] bg-white text-[#5f594d]">
{activeVersion
? `Focus V${activeVersion.versionNumber}`
: "Ready for first pass"}
</Badge>
<AiCadStatusChip
label="Sync"
tone={
workspaceSyncState === "synced"
? "success"
: workspaceSyncState === "error"
? "warning"
: "pending"
}
value={
workspaceSyncState === "synced"
? "Synced"
: workspaceSyncState === "error"
? "Error"
: workspaceSyncState === "loading"
? "Loading"
: "Syncing"
}
/>
</div>
<div className="flex flex-wrap items-center gap-2 text-[#7f7768] text-[11px]">
<span>{versions.length} versions</span>
<span>{completedCount} done</span>
<span>
{hasPendingTasks
? `${pendingTaskCount} task active`
: "Workspace stable"}
</span>
</div>
</div>
{(workspaceSyncError || workspaceOperationError) && (
<div className="mt-3 flex flex-wrap gap-3 text-sm leading-6">
{(workspaceSyncError || workspaceOperationError) && (
<div className="border-[#e6e0d3] border-t px-4 py-2.5">
<div className="flex flex-wrap gap-3 text-sm leading-6">
{workspaceSyncError ? (
<p className="text-rose-700">
云端同步提示:{workspaceSyncError}
@ -3669,132 +3572,114 @@ export function AiCadWorkbench() {
</p>
) : null}
</div>
)}
</div>
</div>
)}
</div>
<div className="min-h-0 flex-1 overflow-hidden">
<div className="flex h-full min-h-0">
<div className="flex w-16 shrink-0 flex-col items-center justify-between border-[#ddd7ca] border-r bg-[#f8f5ee] px-2 py-3">
<div className="flex flex-col items-center gap-3">
<div className="text-[#a39b8b] text-[9px] uppercase tracking-[0.22em]">
Panels
</div>
<div className="flex flex-col items-center gap-2">
<AiCadRailButton
active={
!isHistoryPanelCollapsed &&
activeLeftPanelView === "project"
}
icon={FolderOpen}
label="Project Explorer"
onClick={() => {
historyPanelRef.current?.expand();
setActiveLeftPanelView("project");
}}
shortLabel="Proj"
/>
<AiCadRailButton
active={
!isHistoryPanelCollapsed && activeLeftPanelView === "tree"
}
icon={History}
label="Feature Tree"
onClick={() => {
historyPanelRef.current?.expand();
setActiveLeftPanelView("tree");
}}
shortLabel="Tree"
/>
<AiCadRailButton
active={
!isResultPanelCollapsed && activeResultTab === "preview"
}
icon={Orbit}
label="3D Preview"
onClick={() => {
resultPanelRef.current?.expand();
setActiveResultTab("preview");
}}
shortLabel="3D"
/>
<AiCadRailButton
active={!isResultPanelCollapsed && activeResultTab === "code"}
icon={FileCode2}
label="KCL View"
onClick={() => {
resultPanelRef.current?.expand();
setActiveResultTab("code");
}}
shortLabel="KCL"
/>
<AiCadRailButton
active={!isConversationPanelCollapsed}
icon={BrainCircuit}
label="AI-CAD Agent"
onClick={() => conversationPanelRef.current?.expand()}
shortLabel="AI"
/>
</div>
<div className="flex flex-col items-center gap-2">
<AiCadRailButton
active={
!isHistoryPanelCollapsed && activeLeftPanelView === "project"
}
icon={FolderOpen}
label="Project Explorer"
onClick={() => {
historyPanelRef.current?.expand();
setActiveLeftPanelView("project");
}}
shortLabel="Proj"
/>
<AiCadRailButton
active={
!isHistoryPanelCollapsed && activeLeftPanelView === "tree"
}
icon={History}
label="Feature Tree"
onClick={() => {
historyPanelRef.current?.expand();
setActiveLeftPanelView("tree");
}}
shortLabel="Tree"
/>
<AiCadRailButton
active={
!isResultPanelCollapsed && activeResultTab === "preview"
}
icon={Orbit}
label="3D Preview"
onClick={() => {
resultPanelRef.current?.expand();
setActiveResultTab("preview");
}}
shortLabel="3D"
/>
<AiCadRailButton
active={!isResultPanelCollapsed && activeResultTab === "code"}
icon={FileCode2}
label="KCL View"
onClick={() => {
resultPanelRef.current?.expand();
setActiveResultTab("code");
}}
shortLabel="KCL"
/>
<AiCadRailButton
active={!isConversationPanelCollapsed}
icon={BrainCircuit}
label="AI-CAD Agent"
onClick={() => conversationPanelRef.current?.expand()}
shortLabel="AI"
/>
</div>
<div className="flex flex-col items-center gap-3">
<div className="text-[#a39b8b] text-[9px] uppercase tracking-[0.22em]">
Tools
</div>
<div className="flex flex-col items-center gap-2">
{primaryDownloadEntry ? (
<a
className="flex w-11 flex-col items-center gap-1 rounded-2xl border border-[#ddd7ca] bg-white px-0 py-2.5 text-[#6f695d] transition-all duration-150 ease-out hover:border-[#cfc7b6] hover:bg-[#f6f2ea] hover:text-[#221f18]"
href={primaryDownloadEntry.url}
rel="noreferrer"
target="_blank"
title="导出当前版本"
>
<Download className="h-4 w-4" />
<span className="font-medium text-[9px] uppercase leading-none tracking-[0.18em]">
Export
</span>
</a>
) : (
<button
className="flex w-11 cursor-not-allowed flex-col items-center gap-1 rounded-2xl border border-[#ddd7ca] bg-white px-0 py-2.5 text-[#b8b0a2]"
disabled
title="暂无可导出文件"
type="button"
>
<Download className="h-4 w-4" />
<span className="font-medium text-[9px] uppercase leading-none tracking-[0.18em]">
Export
</span>
</button>
)}
<button
<div className="flex flex-col items-center gap-2">
{primaryDownloadEntry ? (
<a
className="flex w-11 flex-col items-center gap-1 rounded-2xl border border-[#ddd7ca] bg-white px-0 py-2.5 text-[#6f695d] transition-all duration-150 ease-out hover:border-[#cfc7b6] hover:bg-[#f6f2ea] hover:text-[#221f18]"
onClick={handleResetWorkbench}
title="重置当前工作台"
href={primaryDownloadEntry.url}
rel="noreferrer"
target="_blank"
title="导出当前版本"
>
<Download className="h-4 w-4" />
<span className="font-medium text-[9px] uppercase leading-none tracking-[0.18em]">
Export
</span>
</a>
) : (
<button
className="flex w-11 cursor-not-allowed flex-col items-center gap-1 rounded-2xl border border-[#ddd7ca] bg-white px-0 py-2.5 text-[#b8b0a2]"
disabled
title="暂无可导出文件"
type="button"
>
<RotateCcw className="h-4 w-4" />
<Download className="h-4 w-4" />
<span className="font-medium text-[9px] uppercase leading-none tracking-[0.18em]">
Reset
Export
</span>
</button>
</div>
)}
<button
className="flex w-11 flex-col items-center gap-1 rounded-2xl border border-[#ddd7ca] bg-white px-0 py-2.5 text-[#6f695d] transition-all duration-150 ease-out hover:border-[#cfc7b6] hover:bg-[#f6f2ea] hover:text-[#221f18]"
onClick={handleResetWorkbench}
title="重置当前工作台"
type="button"
>
<RotateCcw className="h-4 w-4" />
<span className="font-medium text-[9px] uppercase leading-none tracking-[0.18em]">
Reset
</span>
</button>
</div>
</div>
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<div className="border-[#ddd7ca] border-b bg-[#fdfbf7] px-4 py-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<div className="text-[#8a8272] text-[10px] uppercase tracking-[0.22em]">
Canvas Workspace
</div>
<Badge className="rounded-[8px] border-[#ddd7ca] bg-white text-[#5f594d] text-[10px] uppercase tracking-[0.18em]">
Docked Panels
</Badge>
</div>
<div className="flex flex-wrap items-center gap-2" />
<div className="flex flex-wrap items-center gap-2 text-[#7f7768] text-[11px]">
<span>
{activeVersion
@ -4016,89 +3901,72 @@ export function AiCadWorkbench() {
<CardContent className="flex min-h-0 flex-1 flex-col p-0">
<ScrollArea className="min-h-0 flex-1 px-3.5 py-3.5">
<div className="flex min-h-full flex-col justify-end gap-3">
{messages.map((message) => {
const linkedVersion = message.versionId
? findVersionById(versions, message.versionId)
: null;
{activeVersion || messages.length > 1 ? (
<div className="flex min-h-full flex-col justify-end gap-3">
{messages.map((message) => {
const linkedVersion = message.versionId
? findVersionById(versions, message.versionId)
: null;
return (
<div
className={cn(
"rounded-2xl border px-3 py-2.5 shadow-none transition-all duration-150 ease-out",
message.role === "user"
? "ml-auto max-w-[84%] border-[#bfd9c9] bg-[#ecfbf2] text-[#173225] hover:border-[#a7cfb8]"
: "w-full",
message.role !== "user" &&
getMessageStateClass(message.state)
)}
key={message.id}
>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2 text-[#8a8272] text-[10px] uppercase tracking-[0.18em]">
{message.role === "user" ? (
<>
<WandSparkles className="h-3.5 w-3.5" />
你
</>
) : (
<>
<Bot className="h-3.5 w-3.5" />
AI-CAD Agent
</>
)}
<span>
{formatClockTime(message.createdAt)}
</span>
</div>
{message.role !== "user" &&
message.state ? (
<Badge className="rounded-[8px] border-[#ddd7ca] bg-white text-[#5f594d] text-[10px] uppercase tracking-[0.16em]">
{getMessageStateLabel(message.state)}
</Badge>
) : null}
</div>
{linkedVersion ? (
<div className="mt-2 flex flex-wrap items-center gap-1.5">
<Badge className="border-sky-400/20 bg-sky-400/10 text-sky-100">
V{linkedVersion.versionNumber}
</Badge>
<Badge
className={cn(
"border px-2 py-0 font-medium text-[11px]",
getStatusBadgeClass(
linkedVersion.status
)
return (
<div
className={cn(
"rounded-2xl border px-3 py-2.5 shadow-none transition-all duration-150 ease-out",
message.role === "user"
? "ml-auto max-w-[84%] border-[#ddd7ca] bg-white text-[#201c16]"
: "w-full border-[#e2ddd1] bg-[#f7f5ef] text-[#201c16]"
)}
key={message.id}
>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2 text-[#8a8272] text-[10px] uppercase tracking-[0.18em]">
{message.role === "user" ? (
<>
<WandSparkles className="h-3.5 w-3.5" />
你
</>
) : (
<>
<Bot className="h-3.5 w-3.5" />
AI-CAD Agent
</>
)}
>
{getStatusLabel(linkedVersion.status)}
</Badge>
{linkedVersion.generationMode ===
"iterate" ? (
<Badge className="border-[#ddd7ca] bg-white text-[#5f594d]">
连续修改
<span>
{formatClockTime(message.createdAt)}
</span>
</div>
{message.role !== "user" &&
message.state ? (
<Badge className="rounded-[8px] border-[#ddd7ca] bg-white text-[#5f594d] text-[10px] uppercase tracking-[0.16em]">
{getMessageStateLabel(message.state)}
</Badge>
) : null}
{linkedVersion.parentVersionId ? (
<Badge className="border-[#ddd7ca] bg-white text-[#5f594d]">
linked output
</Badge>
) : (
<Badge className="border-[#ddd7ca] bg-white text-[#5f594d]">
root output
</Badge>
)}
</div>
) : null}
<div className="mt-2 text-[#201c16] text-[13px] leading-[1.55]">
{message.content}
</div>
{linkedVersion ? (
<div className="mt-2 flex flex-wrap items-center gap-1.5">
<Badge className="border-[#ddd7ca] bg-white text-[#5f594d]">
V{linkedVersion.versionNumber}
</Badge>
<Badge
className={cn(
"border px-2 py-0 font-medium text-[11px]",
getStatusBadgeClass(
linkedVersion.status
)
)}
>
{getStatusLabel(linkedVersion.status)}
</Badge>
</div>
) : null}
<div className="mt-3 flex flex-wrap items-center justify-between gap-2 border-[#e6e0d3] border-t pt-2.5">
<div className="flex flex-wrap items-center gap-1.5">
{linkedVersion ? (
<div className="mt-2 text-[13px] leading-[1.65]">
{message.content}
</div>
{linkedVersion ? (
<div className="mt-3 flex flex-wrap items-center gap-1.5 border-[#e6e0d3] border-t pt-2.5">
<AiCadMessageActionButton
icon={ArrowUpRight}
label={`聚焦 V${linkedVersion.versionNumber}`}
@ -4106,143 +3974,89 @@ export function AiCadWorkbench() {
setActiveVersionId(linkedVersion.id)
}
/>
) : null}
{linkedVersion &&
canIterateKittyCadVersion(
linkedVersion
) ? (
<AiCadMessageActionButton
icon={GitBranch}
label="继续修改"
onClick={() =>
handleFocusVersionForIteration(
linkedVersion
)
}
/>
) : null}
<AiCadMessageActionButton
icon={Copy}
label="填入输入框"
onClick={() =>
setPrompt(message.content)
}
/>
</div>
<span className="text-[#8a8272] text-[11px]">
{linkedVersion
? linkedVersion.generationMode ===
"iterate"
? "variant thread"
: "initial thread"
: message.role === "assistant"
? "agent note"
: "prompt draft"}
</span>
{canIterateKittyCadVersion(
linkedVersion
) ? (
<AiCadMessageActionButton
icon={GitBranch}
label="继续修改"
onClick={() =>
handleFocusVersionForIteration(
linkedVersion
)
}
/>
) : null}
</div>
) : null}
</div>
</div>
);
})}
</div>
);
})}
</div>
) : (
<div className="flex min-h-full flex-col justify-end">
<div className="rounded-2xl border border-[#e2ddd1] bg-[#f7f5ef] px-5 py-4 text-[#4d473d]">
<p className="text-[15px] leading-7">
Try requesting a model, ask engineering
questions, or let's explore ideas.
</p>
</div>
</div>
)}
</ScrollArea>
<div className="border-[#e6e0d3] border-t bg-[#f8f5ee] p-3.5">
<div className="mb-2.5 flex flex-wrap items-center gap-2">
<Badge className="border-[#ddd7ca] bg-white text-[#5f594d]">
生成引擎
</Badge>
<Select
onValueChange={(value) =>
setGenerationProvider(
value as CadGenerationProvider
)
}
value={generationProvider}
>
<SelectTrigger className="h-9 w-[220px] border-[#ddd7ca] bg-white text-[#201c16]">
<SelectValue placeholder="选择生成引擎" />
</SelectTrigger>
<SelectContent>
<SelectItem value="kittycad-cad">
KittyCAD / Zoo CAD
</SelectItem>
<SelectItem value="tripo-3d">
Tripo3D(当前回退)
</SelectItem>
</SelectContent>
</Select>
{generationProvider === "tripo-3d" ? (
<>
<Badge className="border-[#ddd7ca] bg-white text-[#5f594d]">
Tripo 模型版本
{activeVersion ? (
<div className="mb-2.5 flex flex-wrap items-center gap-2">
<Badge className="border-[#ddd7ca] bg-white text-[#5f594d]">
生成引擎
</Badge>
<Select
onValueChange={(value) =>
setGenerationProvider(
value as CadGenerationProvider
)
}
value={generationProvider}
>
<SelectTrigger className="h-9 w-[220px] border-[#ddd7ca] bg-white text-[#201c16]">
<SelectValue placeholder="选择生成引擎" />
</SelectTrigger>
<SelectContent>
<SelectItem value="kittycad-cad">
KittyCAD / Zoo CAD
</SelectItem>
<SelectItem value="tripo-3d">
Tripo3D(当前回退)
</SelectItem>
</SelectContent>
</Select>
{generationProvider === "tripo-3d" ? (
<>
<Badge className="border-[#ddd7ca] bg-white text-[#5f594d]">
Tripo 模型版本
</Badge>
<Select
onValueChange={setModelVersion}
value={modelVersion}
>
<SelectTrigger className="h-9 w-[220px] border-[#ddd7ca] bg-white text-[#201c16]">
<SelectValue placeholder="选择模型版本" />
</SelectTrigger>
<SelectContent>
{MODEL_VERSION_OPTIONS.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</>
) : (
<Badge className="border-emerald-200 bg-emerald-50 text-emerald-700">
输出 GLB / STEP / KCL
</Badge>
<Select
onValueChange={setModelVersion}
value={modelVersion}
>
<SelectTrigger className="h-9 w-[220px] border-[#ddd7ca] bg-white text-[#201c16]">
<SelectValue placeholder="选择模型版本" />
</SelectTrigger>
<SelectContent>
{MODEL_VERSION_OPTIONS.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</>
) : (
<Badge className="border-emerald-200 bg-emerald-50 text-emerald-700">
输出 GLB / STEP / KCL
</Badge>
)}
{activeVersion ? (
<Badge className="border-sky-200 bg-sky-50 text-sky-700">
{willSubmitAsIteration
? `基于 V${activeVersion.versionNumber} 连续修改`
: `参考版本 V${activeVersion.versionNumber}`}
</Badge>
) : null}
</div>
{generationProvider === "kittycad-cad" ? (
<div
className={cn(
"mb-2.5 rounded-[8px] border px-3.5 py-2.5 text-[13px] leading-6",
kittycadProviderStatus?.configured
? "border-emerald-200 bg-emerald-50 text-emerald-800"
: "border-amber-200 bg-amber-50 text-amber-800"
)}
>
<div className="flex items-start gap-2">
<AlertCircle className="mt-0.5 h-4 w-4 flex-none" />
<div>
{isLoadingKittycadProviderStatus ? (
<span>
正在检查 KittyCAD CAD 配置状态...
</span>
) : kittycadProviderStatus?.configured ? (
<span>
当前将通过
{kittycadProviderStatus.providerName
? ` ${kittycadProviderStatus.providerName}`
: " KittyCAD CAD"}
发起生成,配置来源:
{getProviderSourceLabel(
kittycadProviderStatus.source
)}
。
</span>
) : (
<span>
还没有检测到 KittyCAD CAD
可用配置。请先到“系统设置 &gt; 本地AI”保存
API Token,或者先切换到 Tripo3D 回退链路。
</span>
)}
</div>
</div>
</div>
) : null}
@ -4277,72 +4091,12 @@ export function AiCadWorkbench() {
</button>
))}
</div>
<div className="mt-4 text-[#8a8272] text-xs uppercase tracking-[0.16em]">
Prompt Directives
</div>
<div className="mt-3 flex flex-wrap gap-2">
<button
className="rounded-xl border border-[#ddd7ca] bg-white px-3 py-1.5 text-left text-[#5f594d] text-xs transition-all duration-150 ease-out hover:bg-[#f4f1e8]"
onClick={handleInsertStructuredTemplate}
type="button"
>
插入完整模板
</button>
{STRUCTURED_PROMPT_DIRECTIVES.map(
(directive) => (
<button
className="rounded-xl border border-[#ddd7ca] bg-white px-3 py-1.5 text-left text-[#5f594d] text-xs transition-all duration-150 ease-out hover:bg-[#f4f1e8]"
key={directive.key}
onClick={() =>
handleInsertStructuredDirective(
directive.key
)
}
type="button"
>
{directive.label}
</button>
)
)}
</div>
{promptStructuredDirectives.length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2">
{promptStructuredDirectives.map(
(directive) => (
<Badge
className="rounded-md border-[#ddd7ca] bg-[#f7f3ea] text-[#5f594d]"
key={directive.key}
>
{directive.label}:
{truncateText(directive.value, 22)}
</Badge>
)
)}
</div>
) : null}
</div>
) : null}
<div className="rounded-2xl border border-[#ddd7ca] bg-white p-3 shadow-[0_1px_0_rgba(255,255,255,0.9)]">
<div className="mb-3 flex flex-wrap items-center justify-between gap-2 border-[#e6e0d3] border-b pb-3">
<div className="flex flex-wrap items-center gap-2">
<span className="text-[#8a8272] text-[10px] uppercase tracking-[0.22em]">
Compose
</span>
<Badge className="rounded-md border-[#ddd7ca] bg-[#f7f3ea] text-[#5f594d]">
{willSubmitAsIteration
? "Iterative edit"
: "New version"}
</Badge>
</div>
<div className="flex flex-wrap items-center gap-2 text-[#8a8272] text-[11px]">
<span>{promptLineCount} lines</span>
<span>{promptCharacterCount} chars</span>
</div>
</div>
<Textarea
className="min-h-[120px] resize-none border-0 bg-transparent px-0 py-0 text-[#201c16] placeholder:text-[#9a9385] focus-visible:ring-0 focus-visible:ring-offset-0"
className="min-h-[128px] resize-none border-0 bg-transparent px-0 py-0 text-[#201c16] placeholder:text-[#9a9385] focus-visible:ring-0 focus-visible:ring-offset-0"
onChange={(event) =>
setPrompt(event.target.value)
}
@ -4352,17 +4106,32 @@ export function AiCadWorkbench() {
swallowAsync(handleSubmit());
}
}}
placeholder="Describe the next geometry change, or say what should stay the same."
placeholder="Create a tabletop 25 mm thick with 1 mm chamfers..."
value={prompt}
/>
<div className="mt-3 flex flex-wrap items-center justify-between gap-3 border-[#e6e0d3] border-t pt-3">
<div className="flex flex-wrap items-center gap-2">
<Badge className="rounded-[8px] border-[#ddd7ca] bg-[#f7f3ea] text-[#5f594d] text-[11px]">
{generationProvider === "kittycad-cad"
? "Fast"
: modelVersion}
</Badge>
<Select
onValueChange={(value) =>
setGenerationProvider(
value as CadGenerationProvider
)
}
value={generationProvider}
>
<SelectTrigger className="h-10 min-w-[148px] rounded-xl border-[#ddd7ca] bg-white text-[#201c16]">
<SelectValue placeholder="选择生成引擎" />
</SelectTrigger>
<SelectContent>
<SelectItem value="kittycad-cad">
Fast
</SelectItem>
<SelectItem value="tripo-3d">
Tripo3D
</SelectItem>
</SelectContent>
</Select>
<Button
className="h-8 w-8 rounded-xl border-[#ddd7ca] bg-white text-[#5f594d] hover:bg-[#f4f1e8] hover:text-[#201c16]"
disabled
@ -4370,7 +4139,7 @@ export function AiCadWorkbench() {
type="button"
variant="outline"
>
<Copy className="h-4 w-4" />
<Paperclip className="h-4 w-4" />
</Button>
<Button
className="h-8 w-8 rounded-xl border-[#ddd7ca] bg-white text-[#5f594d] hover:bg-[#f4f1e8] hover:text-[#201c16]"
@ -4379,40 +4148,26 @@ export function AiCadWorkbench() {
type="button"
variant="outline"
>
<FileCode2 className="h-4 w-4" />
<Camera className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center gap-2">
<Button
className="gap-2 rounded-xl border-[#ddd7ca] bg-white text-[#201c16] transition-all duration-150 ease-out hover:bg-[#f4f1e8]"
onClick={() => setPrompt("")}
type="button"
variant="outline"
>
清空输入
</Button>
<Button
className="gap-2 rounded-xl border border-[#2ccf78] bg-[#31df86] px-4 text-[#102315] transition-all duration-150 ease-out hover:bg-[#2ed57f]"
disabled={isSubmitDisabled}
onClick={() => swallowAsync(handleSubmit())}
type="button"
>
{isSubmitting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
{willSubmitAsIteration
? "继续修改当前版本"
: "生成新版本"}
</Button>
</div>
<Button
className="h-10 w-10 rounded-xl border border-[#2ccf78] bg-[#31df86] p-0 text-[#102315] transition-all duration-150 ease-out hover:bg-[#2ed57f]"
disabled={isSubmitDisabled}
onClick={() => swallowAsync(handleSubmit())}
type="button"
>
{isSubmitting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
</Button>
</div>
<p className="mt-3 text-[#8a8272] text-xs leading-5">
AI-CAD Agent
可能出错,请始终检查尺寸、约束、装配关系和几何结果。
AI-CAD 可能出错。请始终核对尺寸、约束和几何结果。
</p>
</div>
</div>
@ -4434,7 +4189,7 @@ export function AiCadWorkbench() {
order={1}
ref={historyPanelRef}
>
{isHistoryPanelCollapsed ? (
{isHistoryPanelCollapsed || shouldForceTreeCollapsed ? (
<AiCadCollapsedPanel
direction={panelDirection}
icon={
@ -4766,17 +4521,7 @@ export function AiCadWorkbench() {
</div>
) : null}
{versions.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-2xl border border-[#ddd7ca] border-dashed bg-white px-5 py-12 text-center">
<Box className="h-10 w-10 text-[#c3bcaf]" />
<p className="mt-4 font-medium text-[#201c16] text-sm">
还没有版本记录
</p>
<p className="mt-2 text-[#8a8272] text-xs leading-5">
从左侧发起第一轮描述后,这里会自动堆叠版本历史。
</p>
</div>
) : null}
{versions.length === 0 ? null : null}
{versions.map((version) => {
const isActive = version.id === activeVersion?.id;