refactor: simplify ai-cad workbench chrome
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-07 00:36:41 +08:00
parent c74dffdcd6
commit 6d07b1a915

View File

@ -17,10 +17,14 @@ import {
Grid3X3,
History,
Loader2,
MousePointer2,
Orbit,
Pencil,
Plug,
Plus,
RotateCcw,
Send,
Settings2,
ThumbsDown,
ThumbsUp,
Trash2,
@ -57,6 +61,13 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
@ -2336,6 +2347,8 @@ export function AiCadWorkbench() {
const [modelingCutType, setModelingCutType] =
useState<ModelingCutType>("fillet");
const [isModelingBusy, setIsModelingBusy] = useState(false);
const [isModelingToolsDialogOpen, setIsModelingToolsDialogOpen] =
useState(false);
const [messages, setMessages] = useState<ChatMessage[]>(
createWelcomeMessages
);
@ -4269,10 +4282,23 @@ export function AiCadWorkbench() {
: "Ready for first pass"}
</Badge>
{generationProvider === "kittycad-cad" ? (
<AiCadToolbarGroup className="shrink-0" label="Modeling">
<AiCadToolbarGroup className="shrink-0" label="Tools">
<button
className="inline-flex h-7 items-center gap-1.5 rounded-[7px] border border-[#ddd7ca] bg-white px-2.5 text-[#5f594d] text-[12px] transition-all duration-150 ease-out hover:bg-[#faf7ef] disabled:cursor-not-allowed disabled:opacity-50"
disabled={
isModelingBusy || !isOfficialModelingAvailable
}
onClick={() =>
swallowAsync(handleCreateRectangleSketch())
}
type="button"
>
<Pencil className="h-3.5 w-3.5" />
Start Sketch
</button>
<button
className={cn(
"inline-flex h-7 items-center gap-1.5 rounded-[7px] border px-2 text-[12px] transition-all duration-150 ease-out",
"inline-flex h-7 w-7 items-center justify-center rounded-[7px] border transition-all duration-150 ease-out disabled:cursor-not-allowed disabled:opacity-50",
modelingConnectionState === "connected"
? "border-[#bfd9c9] bg-[#ecfbf2] text-[#173225]"
: "border-[#ddd7ca] bg-white text-[#5f594d] hover:bg-[#faf7ef]"
@ -4287,57 +4313,66 @@ export function AiCadWorkbench() {
: ensureModelingSession()
)
}
type="button"
>
{modelingSessionId ? "Connected" : "Connect"}
</button>
<button
className="inline-flex h-7 items-center gap-1.5 rounded-[7px] border border-[#ddd7ca] bg-white px-2 text-[#5f594d] text-[12px] transition-all duration-150 ease-out hover:bg-[#faf7ef]"
disabled={
isModelingBusy || !isOfficialModelingAvailable
}
onClick={() =>
swallowAsync(handleCreateRectangleSketch())
title={
modelingSessionId
? "断开官方 modeling 会话"
: "连接官方 modeling 会话"
}
type="button"
>
Sketch
<Plug className="h-3.5 w-3.5" />
</button>
<button
className="inline-flex h-7 items-center gap-1.5 rounded-[7px] border border-[#ddd7ca] bg-white px-2 text-[#5f594d] text-[12px] transition-all duration-150 ease-out hover:bg-[#faf7ef]"
className="inline-flex h-7 w-7 items-center justify-center rounded-[7px] border border-[#ddd7ca] bg-white text-[#5f594d] transition-all duration-150 ease-out hover:bg-[#faf7ef] disabled:cursor-not-allowed disabled:opacity-50"
disabled={
isModelingBusy ||
!isOfficialModelingAvailable ||
!modelingSketchId
}
onClick={() => swallowAsync(handleExtrudeSketch())}
title="拉伸当前草图"
type="button"
>
Extrude
<Box className="h-3.5 w-3.5" />
</button>
<button
className="inline-flex h-7 items-center gap-1.5 rounded-[7px] border border-[#ddd7ca] bg-white px-2 text-[#5f594d] text-[12px] transition-all duration-150 ease-out hover:bg-[#faf7ef]"
className="inline-flex h-7 w-7 items-center justify-center rounded-[7px] border border-[#ddd7ca] bg-white text-[#5f594d] transition-all duration-150 ease-out hover:bg-[#faf7ef] disabled:cursor-not-allowed disabled:opacity-50"
disabled={
isModelingBusy ||
!isOfficialModelingAvailable ||
!modelingSolidId
}
onClick={() => swallowAsync(handleSelectCurrentBody())}
title="选择当前实体"
type="button"
>
Select
<MousePointer2 className="h-3.5 w-3.5" />
</button>
<button
className="inline-flex h-7 items-center gap-1.5 rounded-[7px] border border-[#ddd7ca] bg-white px-2 text-[#5f594d] text-[12px] transition-all duration-150 ease-out hover:bg-[#faf7ef]"
className="inline-flex h-7 w-7 items-center justify-center rounded-[7px] border border-[#ddd7ca] bg-white text-[#5f594d] transition-all duration-150 ease-out hover:bg-[#faf7ef] disabled:cursor-not-allowed disabled:opacity-50"
disabled={
isModelingBusy ||
!isOfficialModelingAvailable ||
!modelingSolidId
}
onClick={() => swallowAsync(handleCutSelectedEdges())}
title={
modelingCutType === "fillet"
? "对选中边执行圆角"
: "对选中边执行倒角"
}
type="button"
>
{modelingCutType === "fillet" ? "Fillet" : "Chamfer"}
<WandSparkles className="h-3.5 w-3.5" />
</button>
<button
className="inline-flex h-7 w-7 items-center justify-center rounded-[7px] border border-[#ddd7ca] bg-white text-[#5f594d] transition-all duration-150 ease-out hover:bg-[#faf7ef] disabled:cursor-not-allowed disabled:opacity-50"
disabled={!isOfficialModelingAvailable}
onClick={() => setIsModelingToolsDialogOpen(true)}
title="打开官方建模工具参数"
type="button"
>
<Settings2 className="h-3.5 w-3.5" />
</button>
</AiCadToolbarGroup>
) : null}
@ -4627,234 +4662,6 @@ export function AiCadWorkbench() {
</ScrollArea>
<div className="border-[#e6e0d3] border-t bg-white px-4 py-4">
{generationProvider === "kittycad-cad" ? (
<div className="mb-4 rounded-[10px] border border-[#ddd7ca] bg-[#fbfaf6] p-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
<div className="font-medium text-[#201c16] text-sm">
官方建模工具
</div>
<p className="mt-1 text-[#7f7768] text-xs leading-5">
这一组直接走官方{" "}
<code className="rounded bg-white px-1 py-0.5 text-[11px]">
ws/modeling/commands
</code>
,不是前端假按钮。
</p>
</div>
<Badge
className={cn(
"rounded-full px-2 py-0.5 text-[10px]",
modelingConnectionState === "connected"
? "border-emerald-200 bg-emerald-50 text-emerald-700"
: modelingConnectionState === "error"
? "border-rose-200 bg-rose-50 text-rose-700"
: "border-[#ddd7ca] bg-white text-[#5f594d]"
)}
>
{modelingConnectionState}
</Badge>
</div>
<div className="mt-3 grid gap-2 sm:grid-cols-3">
<Input
className="h-9 border-[#ddd7ca] bg-white text-sm"
onChange={(event) =>
setModelingWidth(event.target.value)
}
placeholder="宽度"
value={modelingWidth}
/>
<Input
className="h-9 border-[#ddd7ca] bg-white text-sm"
onChange={(event) =>
setModelingHeight(event.target.value)
}
placeholder="高度"
value={modelingHeight}
/>
<Input
className="h-9 border-[#ddd7ca] bg-white text-sm"
onChange={(event) =>
setModelingDepth(event.target.value)
}
placeholder="拉伸深度"
value={modelingDepth}
/>
</div>
<div className="mt-2 grid gap-2 sm:grid-cols-[minmax(0,1fr)_120px_120px]">
<Input
className="h-9 border-[#ddd7ca] bg-white text-sm"
onChange={(event) =>
setModelingEdgeIndices(event.target.value)
}
placeholder="边索引,例如 0,1,2,3"
value={modelingEdgeIndices}
/>
<Select
onValueChange={(value) =>
setModelingCutType(value as ModelingCutType)
}
value={modelingCutType}
>
<SelectTrigger className="h-9 border-[#ddd7ca] bg-white text-sm">
<SelectValue placeholder="切边类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="fillet">Fillet</SelectItem>
<SelectItem value="chamfer">
Chamfer
</SelectItem>
</SelectContent>
</Select>
<Input
className="h-9 border-[#ddd7ca] bg-white text-sm"
onChange={(event) =>
setModelingCutSize(event.target.value)
}
placeholder="半径 / 距离"
value={modelingCutSize}
/>
</div>
<div className="mt-3 flex flex-wrap gap-2">
<Button
className="h-8 rounded-[8px] border-[#ddd7ca] bg-white text-[#201c16] hover:bg-[#f4f1e8]"
disabled={
isModelingBusy || !isOfficialModelingAvailable
}
onClick={() =>
swallowAsync(
modelingSessionId
? handleDisconnectModelingSession()
: ensureModelingSession()
)
}
size="sm"
type="button"
variant="outline"
>
{modelingSessionId ? "断开会话" : "连接会话"}
</Button>
<Button
className="h-8 rounded-[8px]"
disabled={
isModelingBusy || !isOfficialModelingAvailable
}
onClick={() =>
swallowAsync(handleCreateRectangleSketch())
}
size="sm"
type="button"
variant="outline"
>
草图矩形
</Button>
<Button
className="h-8 rounded-[8px]"
disabled={
isModelingBusy ||
!isOfficialModelingAvailable ||
!modelingSketchId
}
onClick={() =>
swallowAsync(handleExtrudeSketch())
}
size="sm"
type="button"
variant="outline"
>
拉伸
</Button>
<Button
className="h-8 rounded-[8px]"
disabled={
isModelingBusy ||
!isOfficialModelingAvailable ||
!modelingSolidId
}
onClick={() =>
swallowAsync(handleSelectCurrentBody())
}
size="sm"
type="button"
variant="outline"
>
选择当前实体
</Button>
<Button
className="h-8 rounded-[8px]"
disabled={
isModelingBusy ||
!isOfficialModelingAvailable ||
!modelingSolidId
}
onClick={() =>
swallowAsync(handleCutSelectedEdges())
}
size="sm"
type="button"
variant="outline"
>
{modelingCutType === "fillet"
? "执行圆角"
: "执行倒角"}
</Button>
</div>
<div className="mt-3 space-y-1 text-[#7f7768] text-xs leading-5">
<p>
当前平面:
<span className="font-mono text-[#5f594d]">
{truncateIdentifier(modelingPlaneId, 16)}
</span>
</p>
<p>
当前草图:
<span className="font-mono text-[#5f594d]">
{truncateIdentifier(modelingSketchId, 16)}
</span>
,当前实体:
<span className="font-mono text-[#5f594d]">
{truncateIdentifier(modelingSolidId, 16)}
</span>
</p>
<p>
当前选择:
<span className="font-mono text-[#5f594d]">
{modelingSelectedEntityIds.length > 0
? modelingSelectedEntityIds
.map((item) =>
truncateIdentifier(item, 10)
)
.join(", ")
: "未选择"}
</span>
</p>
</div>
{modelingError ? (
<div className="mt-3 rounded-[8px] border border-rose-200 bg-rose-50 px-3 py-2 text-rose-700 text-xs leading-5">
{modelingError}
</div>
) : null}
{modelingLogLines.length > 0 ? (
<div className="mt-3 rounded-[8px] border border-[#e3ddcf] bg-white px-3 py-2">
<div className="text-[#8a8272] text-[11px] uppercase tracking-[0.12em]">
session log
</div>
<div className="mt-2 space-y-1 text-[#5f594d] text-xs leading-5">
{modelingLogLines.slice(0, 3).map((line) => (
<p key={line}>{line}</p>
))}
</div>
</div>
) : null}
</div>
) : null}
<div className="rounded-[8px] border border-[#d7d0c1] bg-white">
<Textarea
className="min-h-[120px] resize-none border-0 bg-transparent px-4 py-4 text-[#201c16] text-[15px] placeholder:text-[#9a9385] focus-visible:ring-0 focus-visible:ring-offset-0"
@ -4891,6 +4698,20 @@ export function AiCadWorkbench() {
</SelectItem>
</SelectContent>
</Select>
{generationProvider === "kittycad-cad" ? (
<Button
className="h-10 w-10 rounded-[8px] border-[#ddd7ca] bg-white p-0 text-[#5f594d] hover:bg-[#f4f1e8]"
onClick={() =>
setIsModelingToolsDialogOpen(true)
}
size="icon"
title="打开官方建模工具"
type="button"
variant="outline"
>
<Settings2 className="h-4 w-4" />
</Button>
) : null}
</div>
<Button
@ -5744,28 +5565,20 @@ export function AiCadWorkbench() {
<div className="flex h-12 items-center justify-between border-[#e6e0d3] border-b px-4">
<div className="flex min-w-0 items-center gap-2.5">
<Orbit className="h-4 w-4 text-[#6f695d]" />
<div className="font-medium text-[15px]">
{activeVersion ? "建模画布" : "Canvas"}
</div>
{activeVersion ? null : (
<div className="truncate text-[#8b8375] text-[12px]">
The center canvas stays focused on the current
model.
</div>
)}
</div>
<div className="flex items-center gap-2">
{activeVersion ? (
<div className="text-[#8b8375] text-[11px]">
Preview-first canvas
</div>
) : null}
<div className="font-medium text-[15px]">Canvas</div>
{activeVersion ? (
<Badge className="rounded-md border-[#ddd7ca] bg-white text-[#5f594d]">
V{activeVersion.versionNumber}
</Badge>
) : null}
</div>
<div className="flex items-center gap-2">
<div className="text-[#8b8375] text-[11px]">
{activeVersion
? "Preview-first canvas"
: "Ready for prompt"}
</div>
</div>
</div>
<div className="min-h-0 flex-1 overflow-hidden p-0">
@ -6878,6 +6691,227 @@ export function AiCadWorkbench() {
</div>
</div>
</div>
<Dialog
onOpenChange={setIsModelingToolsDialogOpen}
open={isModelingToolsDialogOpen}
>
<DialogContent className="max-w-2xl border-[#ddd7ca] bg-[#fbfaf6] p-0 text-[#201c16]">
<DialogHeader className="border-[#e6e0d3] border-b px-6 py-5">
<DialogTitle className="flex items-center gap-2 text-[18px]">
<WandSparkles className="h-4 w-4 text-[#6f695d]" />
官方建模工具
</DialogTitle>
<DialogDescription className="text-[#7f7768] text-sm leading-6">
这里的动作直接调用 Zoo 官方{" "}
<code className="rounded bg-white px-1 py-0.5 text-[#5f594d] text-[11px]">
ws/modeling/commands
</code>
。主界面保持画布优先,这些参数收在弹层里。
</DialogDescription>
</DialogHeader>
<div className="space-y-4 px-6 py-5">
<div className="flex flex-wrap items-center gap-2">
<Badge
className={cn(
"rounded-full px-2 py-0.5 text-[10px]",
modelingConnectionState === "connected"
? "border-emerald-200 bg-emerald-50 text-emerald-700"
: modelingConnectionState === "error"
? "border-rose-200 bg-rose-50 text-rose-700"
: "border-[#ddd7ca] bg-white text-[#5f594d]"
)}
>
{modelingConnectionState}
</Badge>
<Badge className="rounded-full border-[#ddd7ca] bg-white px-2 py-0.5 text-[#5f594d] text-[10px]">
{modelingSessionId ? "session live" : "session idle"}
</Badge>
<Badge className="rounded-full border-[#ddd7ca] bg-white px-2 py-0.5 text-[#5f594d] text-[10px]">
{modelingCutType === "fillet" ? "fillet" : "chamfer"}
</Badge>
</div>
<div className="grid gap-3 sm:grid-cols-3">
<Input
className="h-10 border-[#ddd7ca] bg-white text-sm"
onChange={(event) => setModelingWidth(event.target.value)}
placeholder="宽度"
value={modelingWidth}
/>
<Input
className="h-10 border-[#ddd7ca] bg-white text-sm"
onChange={(event) =>
setModelingHeight(event.target.value)
}
placeholder="高度"
value={modelingHeight}
/>
<Input
className="h-10 border-[#ddd7ca] bg-white text-sm"
onChange={(event) => setModelingDepth(event.target.value)}
placeholder="拉伸深度"
value={modelingDepth}
/>
</div>
<div className="grid gap-3 sm:grid-cols-[minmax(0,1fr)_140px_140px]">
<Input
className="h-10 border-[#ddd7ca] bg-white text-sm"
onChange={(event) =>
setModelingEdgeIndices(event.target.value)
}
placeholder="边索引,例如 0,1,2,3"
value={modelingEdgeIndices}
/>
<Select
onValueChange={(value) =>
setModelingCutType(value as ModelingCutType)
}
value={modelingCutType}
>
<SelectTrigger className="h-10 border-[#ddd7ca] bg-white text-sm">
<SelectValue placeholder="切边类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="fillet">Fillet</SelectItem>
<SelectItem value="chamfer">Chamfer</SelectItem>
</SelectContent>
</Select>
<Input
className="h-10 border-[#ddd7ca] bg-white text-sm"
onChange={(event) =>
setModelingCutSize(event.target.value)
}
placeholder="半径 / 距离"
value={modelingCutSize}
/>
</div>
<div className="flex flex-wrap gap-2">
<Button
className="rounded-full border-[#ddd7ca] bg-white text-[#201c16] hover:bg-[#f4f1e8]"
disabled={isModelingBusy || !isOfficialModelingAvailable}
onClick={() =>
swallowAsync(
modelingSessionId
? handleDisconnectModelingSession()
: ensureModelingSession()
)
}
size="sm"
type="button"
variant="outline"
>
{modelingSessionId ? "断开会话" : "连接会话"}
</Button>
<Button
className="rounded-full border-[#ddd7ca] bg-white text-[#201c16] hover:bg-[#f4f1e8]"
disabled={isModelingBusy || !isOfficialModelingAvailable}
onClick={() =>
swallowAsync(handleCreateRectangleSketch())
}
size="sm"
type="button"
variant="outline"
>
草图矩形
</Button>
<Button
className="rounded-full border-[#ddd7ca] bg-white text-[#201c16] hover:bg-[#f4f1e8]"
disabled={
isModelingBusy ||
!isOfficialModelingAvailable ||
!modelingSketchId
}
onClick={() => swallowAsync(handleExtrudeSketch())}
size="sm"
type="button"
variant="outline"
>
拉伸
</Button>
<Button
className="rounded-full border-[#ddd7ca] bg-white text-[#201c16] hover:bg-[#f4f1e8]"
disabled={
isModelingBusy ||
!isOfficialModelingAvailable ||
!modelingSolidId
}
onClick={() => swallowAsync(handleSelectCurrentBody())}
size="sm"
type="button"
variant="outline"
>
选择当前实体
</Button>
<Button
className="rounded-full border-[#ddd7ca] bg-white text-[#201c16] hover:bg-[#f4f1e8]"
disabled={
isModelingBusy ||
!isOfficialModelingAvailable ||
!modelingSolidId
}
onClick={() => swallowAsync(handleCutSelectedEdges())}
size="sm"
type="button"
variant="outline"
>
{modelingCutType === "fillet" ? "执行圆角" : "执行倒角"}
</Button>
</div>
<div className="rounded-2xl border border-[#e3ddcf] bg-white px-4 py-3 text-[#7f7768] text-xs leading-6">
<p>
当前平面:
<span className="font-mono text-[#5f594d]">
{truncateIdentifier(modelingPlaneId, 16)}
</span>
</p>
<p>
当前草图:
<span className="font-mono text-[#5f594d]">
{truncateIdentifier(modelingSketchId, 16)}
</span>
,当前实体:
<span className="font-mono text-[#5f594d]">
{truncateIdentifier(modelingSolidId, 16)}
</span>
</p>
<p>
当前选择:
<span className="font-mono text-[#5f594d]">
{modelingSelectedEntityIds.length > 0
? modelingSelectedEntityIds
.map((item) => truncateIdentifier(item, 10))
.join(", ")
: "未选择"}
</span>
</p>
</div>
{modelingError ? (
<div className="rounded-[10px] border border-rose-200 bg-rose-50 px-4 py-3 text-rose-700 text-sm leading-6">
{modelingError}
</div>
) : null}
{modelingLogLines.length > 0 ? (
<div className="rounded-[10px] border border-[#e3ddcf] bg-white px-4 py-3">
<div className="text-[#8a8272] text-[11px] uppercase tracking-[0.12em]">
session log
</div>
<div className="mt-2 space-y-1 text-[#5f594d] text-xs leading-5">
{modelingLogLines.slice(0, 4).map((line) => (
<p key={line}>{line}</p>
))}
</div>
</div>
) : null}
</div>
</DialogContent>
</Dialog>
</div>
</div>
</div>