重构策划 Agent 工作台界面
新增始终可见的策划工作区与文件预览 复用 GameAgent 双栏布局并调整审批澄清交互 审批按钮改为文字与图标并保留现有 Runtime 行为
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
import { Check, RotateCcw, X } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { closeDialogOnEscape } from '../../app/dialogs';
|
||||
import type {
|
||||
DesignClarificationRequest,
|
||||
DesignView,
|
||||
DesignWorkspaceEntry,
|
||||
} from '../../app/types';
|
||||
import { ChatMarkdownMessage } from '../../components/ChatMarkdownMessage';
|
||||
|
||||
const PHASE_LABELS: Record<string, string> = {
|
||||
concept: '概念设计',
|
||||
@@ -37,72 +36,71 @@ type DesignAgentSurfaceProps = {
|
||||
|
||||
export function DesignAgentSurface({
|
||||
view,
|
||||
files,
|
||||
previewPath,
|
||||
previewText,
|
||||
busy,
|
||||
error,
|
||||
onApprove,
|
||||
onClarify,
|
||||
onRetry,
|
||||
onOpenFile,
|
||||
onClosePreview,
|
||||
}: DesignAgentSurfaceProps) {
|
||||
const [clarifyText, setClarifyText] = useState('');
|
||||
const phase = view?.session.currentPhase ?? 'concept';
|
||||
const pending = view?.session.pendingApproval;
|
||||
const clarification = view?.session.pendingClarification;
|
||||
return (
|
||||
<section
|
||||
className="plan-gdd-surface design-agent-surface"
|
||||
aria-label="策划"
|
||||
>
|
||||
<div className="plan-gdd-stage-progress">
|
||||
<div className="plan-gdd-stage-progress__header">
|
||||
<section className="design-agent-controls" aria-label="策划阶段控制">
|
||||
<div className="design-agent-controls__header">
|
||||
<div>
|
||||
<span>当前阶段</span>
|
||||
<strong>{PHASE_LABELS[phase] ?? phase}</strong>
|
||||
{view?.running ? <span>进行中</span> : null}
|
||||
</div>
|
||||
{view?.running ? (
|
||||
<span className="design-agent-controls__status">工作中</span>
|
||||
) : null}
|
||||
</div>
|
||||
{error ? (
|
||||
<p className="plan-gdd-stage-progress__delivery-error">{error}</p>
|
||||
) : null}
|
||||
{error ? <p className="design-agent-controls__error">{error}</p> : null}
|
||||
{pending ? (
|
||||
<div className="design-agent-approval">
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => onApprove(pending.requestId, true)}
|
||||
aria-label="批准"
|
||||
>
|
||||
✅
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => onApprove(pending.requestId, false)}
|
||||
aria-label="不批准"
|
||||
>
|
||||
❌
|
||||
</button>
|
||||
<p>当前阶段产物已提交,是否批准进入下一阶段?</p>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => onApprove(pending.requestId, true)}
|
||||
>
|
||||
<Check size={15} aria-hidden="true" />
|
||||
批准
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => onApprove(pending.requestId, false)}
|
||||
>
|
||||
<X size={15} aria-hidden="true" />
|
||||
继续修改
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{clarification ? (
|
||||
<div className="design-agent-clarify">
|
||||
<p>{clarification.question}</p>
|
||||
{clarification.options.map((option, index) => (
|
||||
<button
|
||||
key={`${clarification.requestId}-${index}`}
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => onClarify(clarification, index, clarifyText)}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
))}
|
||||
<div className="design-agent-clarify__options">
|
||||
{clarification.options.map((option, index) => (
|
||||
<button
|
||||
key={`${clarification.requestId}-${index}`}
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => onClarify(clarification, index, clarifyText)}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<textarea
|
||||
rows={2}
|
||||
value={clarifyText}
|
||||
disabled={busy}
|
||||
placeholder="补充说明(可选)"
|
||||
onChange={(event) => setClarifyText(event.currentTarget.value)}
|
||||
/>
|
||||
{clarification.options.length === 0 ? (
|
||||
@@ -111,57 +109,21 @@ export function DesignAgentSurface({
|
||||
disabled={busy || !clarifyText.trim()}
|
||||
onClick={() => onClarify(clarification, null, clarifyText)}
|
||||
>
|
||||
发送
|
||||
提交回答
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{view?.canRetry ? (
|
||||
<div className="design-agent-approval">
|
||||
<button type="button" disabled={busy} onClick={onRetry}>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="design-agent-files">
|
||||
{files.map((file) =>
|
||||
file.kind === 'directory' ? (
|
||||
<div key={file.path} className="design-agent-file is-dir">
|
||||
{file.path}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
key={file.path}
|
||||
type="button"
|
||||
className="design-agent-file"
|
||||
onClick={() => onOpenFile(file.path)}
|
||||
>
|
||||
{file.path}
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
{previewPath ? (
|
||||
<div
|
||||
className="design-agent-preview"
|
||||
role="dialog"
|
||||
aria-label={previewPath}
|
||||
onKeyDown={(event) => closeDialogOnEscape(event, onClosePreview)}
|
||||
<button
|
||||
className="design-agent-retry"
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={onRetry}
|
||||
>
|
||||
<header>
|
||||
<strong>{previewPath}</strong>
|
||||
<button type="button" onClick={onClosePreview} aria-label="关闭">
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
<div className="design-agent-preview__body">
|
||||
{previewPath.endsWith('.md') ? (
|
||||
<ChatMarkdownMessage role="assistant" text={previewText} />
|
||||
) : (
|
||||
<pre>{previewText}</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RotateCcw size={15} aria-hidden="true" />
|
||||
重试本轮
|
||||
</button>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
import { ArrowLeft, Check, File, Folder, RefreshCw } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { resolveTauriInvoke } from '../../app/tauri';
|
||||
import type { DesignView, DesignWorkspaceEntry } from '../../app/types';
|
||||
import { ChatMarkdownMessage } from '../../components/ChatMarkdownMessage';
|
||||
|
||||
const PHASES = [
|
||||
['concept', '概念设计'],
|
||||
['top_design', '顶层设计'],
|
||||
['architecture', '系统架构'],
|
||||
['systems', '系统文档'],
|
||||
['tdd', '技术文档'],
|
||||
] as const;
|
||||
|
||||
type DesignWorkspacePanelProps = {
|
||||
projectPath: string;
|
||||
};
|
||||
|
||||
function phaseLabel(phase: string) {
|
||||
return PHASES.find(([id]) => id === phase)?.[1] ?? phase;
|
||||
}
|
||||
|
||||
function pathDepth(path: string) {
|
||||
return path.split(/[\\/]/u).filter(Boolean).length - 1;
|
||||
}
|
||||
|
||||
export function DesignWorkspacePanel({
|
||||
projectPath,
|
||||
}: DesignWorkspacePanelProps) {
|
||||
const [view, setView] = useState<DesignView | null>(null);
|
||||
const [files, setFiles] = useState<DesignWorkspaceEntry[]>([]);
|
||||
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
||||
const [previewText, setPreviewText] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const loadWorkspace = useCallback(async () => {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke || !projectPath.trim()) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const [nextView, nextFiles] = await Promise.all([
|
||||
invoke<DesignView | null>('hydrate_design_agent_session', {
|
||||
projectPath,
|
||||
}),
|
||||
invoke<DesignWorkspaceEntry[]>('list_design_workspace', {
|
||||
projectPath,
|
||||
}),
|
||||
]);
|
||||
setView(nextView);
|
||||
setFiles(nextFiles);
|
||||
setSelectedPath((current) =>
|
||||
current &&
|
||||
nextFiles.some(
|
||||
(entry) => entry.kind !== 'directory' && entry.path === current,
|
||||
)
|
||||
? current
|
||||
: null,
|
||||
);
|
||||
} catch (nextError) {
|
||||
setError(
|
||||
nextError instanceof Error ? nextError.message : String(nextError),
|
||||
);
|
||||
setFiles([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectPath]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadWorkspace();
|
||||
}, [loadWorkspace]);
|
||||
|
||||
useEffect(() => {
|
||||
const listen = window.__TAURI__?.event?.listen;
|
||||
if (!listen || !projectPath.trim()) {
|
||||
return;
|
||||
}
|
||||
let disposed = false;
|
||||
let cleanup: (() => void) | null = null;
|
||||
void listen<{ projectPath: string; kind: string }>(
|
||||
'design-agent-update',
|
||||
(event) => {
|
||||
if (!disposed && event.payload.projectPath === projectPath) {
|
||||
void loadWorkspace();
|
||||
}
|
||||
},
|
||||
)
|
||||
.then((unlisten) => {
|
||||
if (disposed) {
|
||||
unlisten();
|
||||
} else {
|
||||
cleanup = unlisten;
|
||||
}
|
||||
})
|
||||
.catch(() => undefined);
|
||||
return () => {
|
||||
disposed = true;
|
||||
cleanup?.();
|
||||
};
|
||||
}, [loadWorkspace, projectPath]);
|
||||
|
||||
const sortedFiles = useMemo(
|
||||
() =>
|
||||
[...files].sort((left, right) => {
|
||||
if (left.kind !== right.kind) {
|
||||
return left.kind === 'directory' ? -1 : 1;
|
||||
}
|
||||
return left.path.localeCompare(right.path, 'zh-CN');
|
||||
}),
|
||||
[files],
|
||||
);
|
||||
|
||||
const openFile = useCallback(
|
||||
async (path: string) => {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
return;
|
||||
}
|
||||
setSelectedPath(path);
|
||||
setPreviewLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const content = await invoke<string>('read_design_workspace_file', {
|
||||
projectPath,
|
||||
path,
|
||||
});
|
||||
setPreviewText(content);
|
||||
} catch (nextError) {
|
||||
setPreviewText('');
|
||||
setError(
|
||||
nextError instanceof Error ? nextError.message : String(nextError),
|
||||
);
|
||||
} finally {
|
||||
setPreviewLoading(false);
|
||||
}
|
||||
},
|
||||
[projectPath],
|
||||
);
|
||||
|
||||
const currentPhase = view?.session.currentPhase ?? 'concept';
|
||||
const currentPhaseIndex = Math.max(
|
||||
0,
|
||||
PHASES.findIndex(([id]) => id === currentPhase),
|
||||
);
|
||||
const selectedEntry = selectedPath
|
||||
? files.find((entry) => entry.path === selectedPath)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<section className="design-workspace-panel" aria-label="策划工作区">
|
||||
<header className="design-workspace-panel__header">
|
||||
<div>
|
||||
<span className="design-workspace-panel__eyebrow">策划工作区</span>
|
||||
<h1>{phaseLabel(currentPhase)}</h1>
|
||||
<p>Agent 产生的策划文档会显示在这里。</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="design-workspace-panel__refresh"
|
||||
onClick={() => void loadWorkspace()}
|
||||
disabled={loading}
|
||||
aria-label="刷新策划工作区"
|
||||
>
|
||||
<RefreshCw size={15} aria-hidden="true" />
|
||||
刷新
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<nav className="design-phase-rail" aria-label="策划阶段">
|
||||
{PHASES.map(([id, label], index) => (
|
||||
<div
|
||||
key={id}
|
||||
className={`design-phase-rail__item${
|
||||
index < currentPhaseIndex ? ' is-complete' : ''
|
||||
}${index === currentPhaseIndex ? ' is-current' : ''}`}
|
||||
>
|
||||
<span className="design-phase-rail__marker">
|
||||
{index < currentPhaseIndex ? (
|
||||
<Check size={12} aria-hidden="true" />
|
||||
) : (
|
||||
index + 1
|
||||
)}
|
||||
</span>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{error ? (
|
||||
<p className="design-workspace-panel__error" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="design-workspace-panel__body">
|
||||
<aside className="design-workspace-tree" aria-label="策划文件">
|
||||
<header>
|
||||
<strong>工作区文件</strong>
|
||||
<span>
|
||||
{files.filter((entry) => entry.kind !== 'directory').length}{' '}
|
||||
个文件
|
||||
</span>
|
||||
</header>
|
||||
{loading ? (
|
||||
<p className="design-workspace-empty">正在读取工作区…</p>
|
||||
) : sortedFiles.length === 0 ? (
|
||||
<div className="design-workspace-empty">
|
||||
<Folder size={24} aria-hidden="true" />
|
||||
<strong>工作区还没有文件</strong>
|
||||
<span>开始对话后,Agent 创建的文档会出现在这里。</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="design-workspace-tree__list">
|
||||
{sortedFiles.map((entry) => {
|
||||
const isDirectory = entry.kind === 'directory';
|
||||
return isDirectory ? (
|
||||
<div
|
||||
key={entry.path}
|
||||
className="design-workspace-tree__entry is-directory"
|
||||
style={{
|
||||
paddingLeft: `${12 + pathDepth(entry.path) * 12}px`,
|
||||
}}
|
||||
>
|
||||
<Folder size={15} aria-hidden="true" />
|
||||
<span>{entry.path}</span>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
key={entry.path}
|
||||
type="button"
|
||||
className={`design-workspace-tree__entry${selectedPath === entry.path ? ' is-selected' : ''}`}
|
||||
style={{
|
||||
paddingLeft: `${12 + pathDepth(entry.path) * 12}px`,
|
||||
}}
|
||||
onClick={() => void openFile(entry.path)}
|
||||
>
|
||||
<File size={15} aria-hidden="true" />
|
||||
<span>{entry.path}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
<article className="design-workspace-preview" aria-label="策划文件预览">
|
||||
{selectedEntry && selectedPath ? (
|
||||
<>
|
||||
<header>
|
||||
<button type="button" onClick={() => setSelectedPath(null)}>
|
||||
<ArrowLeft size={15} aria-hidden="true" />
|
||||
文件列表
|
||||
</button>
|
||||
<strong>{selectedPath}</strong>
|
||||
</header>
|
||||
{previewLoading ? (
|
||||
<p className="design-workspace-empty">正在读取文件…</p>
|
||||
) : selectedPath.toLowerCase().endsWith('.md') ? (
|
||||
<div className="design-workspace-preview__markdown">
|
||||
<ChatMarkdownMessage role="assistant" text={previewText} />
|
||||
</div>
|
||||
) : (
|
||||
<pre>{previewText}</pre>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="design-workspace-preview__placeholder">
|
||||
<File size={28} aria-hidden="true" />
|
||||
<strong>选择一个文件查看内容</strong>
|
||||
<span>文档会在这里按普通 Markdown 方式预览。</span>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -401,7 +401,8 @@ export function ProjectSupervisorView({
|
||||
runtimePanelProps.controlBusy ||
|
||||
needsUserInput ||
|
||||
modelValidating ||
|
||||
Boolean(designView?.session.pendingApproval)
|
||||
Boolean(designView?.session.pendingApproval) ||
|
||||
Boolean(designView?.session.pendingClarification)
|
||||
}
|
||||
rows={3}
|
||||
value={chatInput}
|
||||
@@ -440,6 +441,7 @@ export function ProjectSupervisorView({
|
||||
runtimePanelProps.controlBusy ||
|
||||
needsUserInput ||
|
||||
Boolean(designView?.session.pendingApproval) ||
|
||||
Boolean(designView?.session.pendingClarification) ||
|
||||
(directCodex && (!modelReady || modelValidating))
|
||||
}
|
||||
>
|
||||
|
||||
@@ -8171,6 +8171,387 @@ iframe.preview-frame {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* 策划 Agent 使用 GameAgent 工作台的两栏外壳。左侧工作区即使为空也保持可见,
|
||||
右侧只承载对话与阶段控制。 */
|
||||
.game-project-workbench--design {
|
||||
--game-workbench-stage-fill: #fffdfa;
|
||||
--game-workbench-agent-fill: #fbf7f2;
|
||||
}
|
||||
|
||||
.game-workbench-layout--design {
|
||||
height: clamp(600px, calc(100dvh - 154px), 900px);
|
||||
grid-template-columns: minmax(0, 1fr) minmax(380px, 0.42fr);
|
||||
}
|
||||
|
||||
.game-workbench-layout--design .game-workbench-stage {
|
||||
display: block;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.game-workbench-layout--design .game-workbench-chat {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.design-workspace-panel {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto minmax(0, 1fr);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 22px;
|
||||
overflow: hidden;
|
||||
background: var(--game-workbench-stage-fill);
|
||||
}
|
||||
|
||||
.design-workspace-panel__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.design-workspace-panel__eyebrow {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: var(--platform-text-soft);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.design-workspace-panel__header h1 {
|
||||
margin: 0;
|
||||
color: var(--platform-text-strong);
|
||||
font-size: clamp(22px, 2.4vw, 32px);
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.design-workspace-panel__header p {
|
||||
margin-top: 8px;
|
||||
color: var(--platform-text-base);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.design-workspace-panel__refresh,
|
||||
.design-workspace-preview header button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 34px;
|
||||
padding: 0 11px;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
border-radius: 8px;
|
||||
background: var(--platform-button-secondary-fill);
|
||||
color: var(--platform-button-secondary-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.design-workspace-panel__refresh:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.design-phase-rail {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 12px 0 18px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.design-phase-rail__item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
flex: 0 0 auto;
|
||||
min-height: 30px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--platform-line-soft);
|
||||
border-radius: 999px;
|
||||
color: var(--platform-text-soft);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.design-phase-rail__item.is-current {
|
||||
border-color: var(--platform-button-primary-border);
|
||||
background: var(--platform-warm-bg);
|
||||
color: var(--platform-text-strong);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.design-phase-rail__item.is-complete {
|
||||
color: var(--platform-text-base);
|
||||
}
|
||||
|
||||
.design-phase-rail__marker {
|
||||
display: inline-grid;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: var(--platform-line-soft);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.design-phase-rail__item.is-current .design-phase-rail__marker,
|
||||
.design-phase-rail__item.is-complete .design-phase-rail__marker {
|
||||
background: var(--platform-button-primary-fill);
|
||||
color: var(--platform-button-primary-text);
|
||||
}
|
||||
|
||||
.design-workspace-panel__error,
|
||||
.design-agent-controls__error {
|
||||
margin: 0 0 10px;
|
||||
color: var(--platform-danger-text, #b42318);
|
||||
font-size: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.design-workspace-panel__body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 0.34fr) minmax(0, 1fr);
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--platform-line-soft);
|
||||
border-radius: 12px;
|
||||
background: var(--platform-input-fill);
|
||||
}
|
||||
|
||||
.design-workspace-tree {
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
border-right: 1px solid var(--platform-line-soft);
|
||||
}
|
||||
|
||||
.design-workspace-tree > header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--platform-line-soft);
|
||||
background: var(--platform-neutral-bg);
|
||||
}
|
||||
|
||||
.design-workspace-tree > header strong {
|
||||
color: var(--platform-text-strong);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.design-workspace-tree > header span {
|
||||
color: var(--platform-text-soft);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.design-workspace-tree__list {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.design-workspace-tree__entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-height: 34px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: var(--platform-text-base);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.design-workspace-tree__entry span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
button.design-workspace-tree__entry {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.design-workspace-tree__entry:hover,
|
||||
.design-workspace-tree__entry.is-selected {
|
||||
background: var(--platform-warm-bg);
|
||||
color: var(--platform-text-strong);
|
||||
}
|
||||
|
||||
.design-workspace-tree__entry.is-directory {
|
||||
color: var(--platform-text-soft);
|
||||
}
|
||||
|
||||
.design-workspace-empty,
|
||||
.design-workspace-preview__placeholder {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 8px;
|
||||
min-height: 150px;
|
||||
padding: 24px;
|
||||
color: var(--platform-text-soft);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.design-workspace-empty strong,
|
||||
.design-workspace-preview__placeholder strong {
|
||||
color: var(--platform-text-strong);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.design-workspace-preview {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--platform-neutral-bg);
|
||||
}
|
||||
|
||||
.design-workspace-preview > header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--platform-line-soft);
|
||||
}
|
||||
|
||||
.design-workspace-preview > header strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--platform-text-strong);
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.design-workspace-preview__markdown,
|
||||
.design-workspace-preview > pre {
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 18px;
|
||||
overflow: auto;
|
||||
color: var(--platform-text-base);
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.design-agent-controls {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--platform-line-soft);
|
||||
border-radius: 10px;
|
||||
background: var(--platform-neutral-bg);
|
||||
}
|
||||
|
||||
.design-agent-controls__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.design-agent-controls__header div {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.design-agent-controls__header span:first-child {
|
||||
color: var(--platform-text-soft);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.design-agent-controls__header strong {
|
||||
color: var(--platform-text-strong);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.design-agent-controls__status {
|
||||
color: var(--platform-accent);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.design-agent-approval,
|
||||
.design-agent-clarify {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.design-agent-approval p,
|
||||
.design-agent-clarify p {
|
||||
margin: 0;
|
||||
color: var(--platform-text-base);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.design-agent-approval > div,
|
||||
.design-agent-clarify__options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.design-agent-approval button,
|
||||
.design-agent-clarify button,
|
||||
.design-agent-retry {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-height: 34px;
|
||||
padding: 0 11px;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
border-radius: 8px;
|
||||
background: var(--platform-button-secondary-fill);
|
||||
color: var(--platform-button-secondary-text);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.design-agent-approval button:first-child {
|
||||
border-color: var(--platform-button-primary-border);
|
||||
background: var(--platform-button-primary-fill);
|
||||
color: var(--platform-button-primary-text);
|
||||
}
|
||||
|
||||
.design-agent-approval button:disabled,
|
||||
.design-agent-clarify button:disabled,
|
||||
.design-agent-retry:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.design-agent-clarify textarea {
|
||||
width: 100%;
|
||||
min-height: 58px;
|
||||
padding: 9px 10px;
|
||||
resize: vertical;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
border-radius: 8px;
|
||||
background: var(--platform-input-fill);
|
||||
color: var(--platform-text-base);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.design-agent-retry {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.gdd-approval-card__header {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
type LocalAssetCommittedEvent,
|
||||
type TauriImageCanvasHostAdapter,
|
||||
} from '../../features/asset-canvas/tauriImageCanvasHostAdapter';
|
||||
import { DesignWorkspacePanel } from '../../features/project-workspace/DesignWorkspacePanel';
|
||||
import {
|
||||
LocalGamePreviewFrame,
|
||||
resolveEmbeddedPreviewUrl,
|
||||
@@ -4209,6 +4210,33 @@ export default function ProjectDevelopmentView({
|
||||
!resourceEditorRoute &&
|
||||
!uiEditorRoute;
|
||||
|
||||
if (planningStartMode) {
|
||||
return (
|
||||
<section
|
||||
className="launcher-page launcher-project-development game-project-workbench game-project-workbench--design"
|
||||
aria-label="策划工作台"
|
||||
>
|
||||
<div className="game-workbench-layout game-workbench-layout--design">
|
||||
<section className="game-workbench-stage" aria-label="策划工作区">
|
||||
<DesignWorkspacePanel projectPath={projectPath} />
|
||||
</section>
|
||||
<aside className="game-workbench-chat" aria-label="策划 Agent 对话">
|
||||
<header>
|
||||
<div className="game-workbench-chat-title">
|
||||
<strong>与策划 Agent 对话</strong>
|
||||
<small>持续协作推进设计</small>
|
||||
</div>
|
||||
{walletEntry ? (
|
||||
<div className="game-workbench-chat-wallet">{walletEntry}</div>
|
||||
) : null}
|
||||
</header>
|
||||
{supervisor}
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
className="launcher-page launcher-project-development game-project-workbench"
|
||||
|
||||
Reference in New Issue
Block a user