feat: scaffold agentic interior design workflow
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
"use client";
|
||||
|
||||
import * as Tabs from "@radix-ui/react-tabs";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
ArrowsOutIcon,
|
||||
BuildingsIcon,
|
||||
CheckIcon,
|
||||
CircleNotchIcon,
|
||||
CubeIcon,
|
||||
EyeIcon,
|
||||
EyeSlashIcon,
|
||||
ImageIcon,
|
||||
LockSimpleIcon,
|
||||
PaperPlaneTiltIcon,
|
||||
PlusIcon,
|
||||
SlidersHorizontalIcon,
|
||||
SparkleIcon,
|
||||
UploadSimpleIcon,
|
||||
WarningCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import Image from "next/image";
|
||||
import { FormEvent, useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
initialLayers,
|
||||
initialMessages,
|
||||
styleDirections,
|
||||
workflowStages,
|
||||
} from "@/lib/demo";
|
||||
import type { ChatMessage } from "@/types/workflow";
|
||||
|
||||
const iconWeight = "regular" as const;
|
||||
|
||||
export function WorkflowStudio() {
|
||||
const [layers, setLayers] = useState(initialLayers);
|
||||
const [selectedDirection, setSelectedDirection] = useState("quiet-modern");
|
||||
const [messages, setMessages] = useState<ChatMessage[]>(initialMessages);
|
||||
const [draft, setDraft] = useState("");
|
||||
const [furnitureMode, setFurnitureMode] = useState<"reference" | "remove">("reference");
|
||||
|
||||
const currentDirection = useMemo(
|
||||
() => styleDirections.find((item) => item.id === selectedDirection) ?? styleDirections[0],
|
||||
[selectedDirection],
|
||||
);
|
||||
|
||||
function toggleLayer(id: string) {
|
||||
setLayers((items) =>
|
||||
items.map((item) => (item.id === id ? { ...item, visible: !item.visible } : item)),
|
||||
);
|
||||
}
|
||||
|
||||
function sendMessage(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const value = draft.trim();
|
||||
if (!value) return;
|
||||
setMessages((items) => [
|
||||
...items,
|
||||
{ id: `user-${Date.now()}`, role: "user", body: value },
|
||||
{
|
||||
id: `assistant-${Date.now()}`,
|
||||
role: "assistant",
|
||||
body: "已记录为设计约束。正式接入后,这条指令会先更新布局锁定和 Style DNA,再只重绘受影响视角。",
|
||||
meta: "结构化修改",
|
||||
},
|
||||
]);
|
||||
setDraft("");
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip.Provider delayDuration={250}>
|
||||
<main className="studio-shell">
|
||||
<header className="topbar">
|
||||
<div className="brand-block">
|
||||
<div className="brand-mark" aria-hidden="true">
|
||||
<BuildingsIcon size={19} weight="fill" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="brand-name">空间风格工作台</p>
|
||||
<p className="brand-subtitle">11-2-104 住宅概念方案</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="topbar-actions">
|
||||
<span className="save-state"><CheckIcon size={14} weight="bold" /> 已保存</span>
|
||||
<button className="button button-secondary" type="button">版本记录</button>
|
||||
<button className="button button-primary" type="button">
|
||||
确认结构 <ArrowRightIcon size={16} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="studio-grid">
|
||||
<aside className="workflow-rail" aria-label="设计流程">
|
||||
<div className="rail-heading">
|
||||
<p>设计流程</p>
|
||||
<span>3 / 8</span>
|
||||
</div>
|
||||
<nav className="stage-list">
|
||||
{workflowStages.map((stage) => (
|
||||
<button
|
||||
className={`stage-item stage-${stage.status}`}
|
||||
key={stage.id}
|
||||
type="button"
|
||||
aria-current={stage.status === "active" ? "step" : undefined}
|
||||
>
|
||||
<span className="stage-index" aria-hidden="true">
|
||||
{stage.status === "complete" ? <CheckIcon size={13} weight="bold" /> : null}
|
||||
</span>
|
||||
<span className="stage-copy">
|
||||
<strong>{stage.label}</strong>
|
||||
<small>{stage.detail}</small>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="rail-note">
|
||||
<WarningCircleIcon size={18} weight={iconWeight} />
|
||||
<p>有 2 项需要确认,确认后才能生成空间白模。</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section className="workspace">
|
||||
<Tabs.Root defaultValue="plan" className="workspace-tabs">
|
||||
<div className="workspace-toolbar">
|
||||
<Tabs.List className="tab-list" aria-label="工作区视图">
|
||||
<Tabs.Trigger className="tab-trigger" value="plan">
|
||||
<SlidersHorizontalIcon size={16} /> 户型清洗
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger className="tab-trigger" value="blockout">
|
||||
<CubeIcon size={16} /> 3D 白模
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger className="tab-trigger" value="renders">
|
||||
<ImageIcon size={16} /> 效果图
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<div className="canvas-actions">
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button className="icon-button" type="button" aria-label="适应画布">
|
||||
<ArrowsOutIcon size={18} />
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal><Tooltip.Content className="tooltip">适应画布</Tooltip.Content></Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
<button className="button button-secondary compact" type="button">
|
||||
<UploadSimpleIcon size={16} /> 更换图纸
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs.Content value="plan" className="tab-content">
|
||||
<div className="plan-workspace">
|
||||
<div className="plan-canvas">
|
||||
<div className="canvas-labels">
|
||||
<span>上方住宅户型</span>
|
||||
<span>矢量 PDF · 43 个 CAD 图层</span>
|
||||
</div>
|
||||
<div className="plan-image-frame">
|
||||
<Image
|
||||
src="/sample-plan.png"
|
||||
alt="从真实 PDF 中分离出的住宅建筑结构平面图"
|
||||
fill
|
||||
priority
|
||||
sizes="(max-width: 900px) 100vw, 60vw"
|
||||
className="plan-image"
|
||||
/>
|
||||
<div className="region-outline" aria-hidden="true" />
|
||||
<button className="issue-marker issue-one" type="button" aria-label="比例待确认">1</button>
|
||||
<button className="issue-marker issue-two" type="button" aria-label="家具意图待确认">2</button>
|
||||
</div>
|
||||
<div className="canvas-status">
|
||||
<span><CheckIcon size={14} weight="bold" /> 已分离建筑结构</span>
|
||||
<span>比例待交叉校准</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="layer-panel">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<p className="panel-title">图层</p>
|
||||
<p className="panel-caption">保留矢量信息</p>
|
||||
</div>
|
||||
<button className="icon-button small" type="button" aria-label="添加图层">
|
||||
<PlusIcon size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="layer-list">
|
||||
{layers.map((layer) => (
|
||||
<button
|
||||
className={`layer-row ${layer.visible ? "is-visible" : ""}`}
|
||||
key={layer.id}
|
||||
type="button"
|
||||
onClick={() => toggleLayer(layer.id)}
|
||||
>
|
||||
{layer.visible ? <EyeIcon size={17} /> : <EyeSlashIcon size={17} />}
|
||||
<span>{layer.label}</span>
|
||||
{layer.count ? <small>{layer.count}</small> : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="panel-divider" />
|
||||
<p className="panel-title">原家具处理</p>
|
||||
<div className="segmented-control">
|
||||
<button
|
||||
type="button"
|
||||
className={furnitureMode === "reference" ? "active" : ""}
|
||||
onClick={() => setFurnitureMode("reference")}
|
||||
>保留参考</button>
|
||||
<button
|
||||
type="button"
|
||||
className={furnitureMode === "remove" ? "active" : ""}
|
||||
onClick={() => setFurnitureMode("remove")}
|
||||
>仅看结构</button>
|
||||
</div>
|
||||
<p className="helper-text">家具位置用于理解尺度,不锁定款式和材质。</p>
|
||||
</aside>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="blockout" className="tab-content empty-workspace">
|
||||
<CubeIcon size={38} weight="thin" />
|
||||
<h2>白模将在结构确认后生成</h2>
|
||||
<p>系统会拉伸墙体、门窗和柱,并自动建立三个室内相机。</p>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="renders" className="tab-content empty-workspace">
|
||||
<ImageIcon size={38} weight="thin" />
|
||||
<h2>先选定风格方向</h2>
|
||||
<p>低成本方向图通过后,再生成完整多视角效果图。</p>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
||||
<section className="conversation" aria-label="设计对话">
|
||||
<div className="message-stream">
|
||||
{messages.slice(-3).map((message) => (
|
||||
<article className={`message message-${message.role}`} key={message.id}>
|
||||
{message.role === "assistant" ? (
|
||||
<span className="assistant-icon"><SparkleIcon size={15} weight="fill" /></span>
|
||||
) : null}
|
||||
<div>
|
||||
{message.meta ? <small>{message.meta}</small> : null}
|
||||
<p>{message.body}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<form className="composer" onSubmit={sendMessage}>
|
||||
<label htmlFor="design-instruction">告诉我需要保留什么,或者希望空间给人怎样的感受</label>
|
||||
<div className="composer-row">
|
||||
<input
|
||||
id="design-instruction"
|
||||
value={draft}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
placeholder="例如:保留床和窗的位置,客厅更松弛,不要冷灰"
|
||||
/>
|
||||
<button className="send-button" type="submit" aria-label="发送设计指令">
|
||||
<PaperPlaneTiltIcon size={18} weight="fill" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<aside className="style-inspector">
|
||||
<div className="inspector-heading">
|
||||
<div>
|
||||
<p className="panel-title">Style DNA</p>
|
||||
<p className="panel-caption">跨视角设计约束</p>
|
||||
</div>
|
||||
<span className="lock-label"><LockSimpleIcon size={13} /> 2 项锁定</span>
|
||||
</div>
|
||||
|
||||
<div className="style-section">
|
||||
<label htmlFor="style-concept">设计概念</label>
|
||||
<textarea id="style-concept" defaultValue="温暖、克制、带自然材质感的现代住宅" />
|
||||
</div>
|
||||
|
||||
<div className="style-section">
|
||||
<p className="style-label">方向草案</p>
|
||||
<div className="direction-list">
|
||||
{styleDirections.map((direction) => (
|
||||
<button
|
||||
type="button"
|
||||
key={direction.id}
|
||||
className={`direction-option ${selectedDirection === direction.id ? "selected" : ""}`}
|
||||
onClick={() => setSelectedDirection(direction.id)}
|
||||
>
|
||||
<span className="direction-colors" aria-hidden="true">
|
||||
{direction.colors.map((color) => (
|
||||
<span key={color} style={{ backgroundColor: color }} />
|
||||
))}
|
||||
</span>
|
||||
<span>
|
||||
<strong>{direction.name}</strong>
|
||||
<small>{direction.summary}</small>
|
||||
</span>
|
||||
{selectedDirection === direction.id ? <CheckIcon size={15} weight="bold" /> : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="style-section material-section">
|
||||
<p className="style-label">材料语言</p>
|
||||
<div className="tag-list">
|
||||
{currentDirection.materials.map((material) => <span key={material}>{material}</span>)}
|
||||
<button type="button"><PlusIcon size={13} /> 添加</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="style-section constraint-section">
|
||||
<p className="style-label">明确避免</p>
|
||||
<p>大面积冷灰、高亮岩板、过量灯带</p>
|
||||
</div>
|
||||
|
||||
<div className="inspector-footer">
|
||||
<div>
|
||||
<CircleNotchIcon size={17} />
|
||||
<span>等待结构确认</span>
|
||||
</div>
|
||||
<button className="button button-primary full" type="button">保存风格草案</button>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user