refactor: streamline AI-CAD into single-screen workspace
This commit is contained in:
parent
9d222f0d42
commit
12bc8b1668
@ -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
|
||||
可用配置。请先到“系统设置 > 本地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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user