优化 Agent 对话与运行入口

优化 Agent 消息排版与悬浮输入框
隐藏资源管理搜索框并调整运行入口
恢复运行视窗播放按钮位置与箭头样式
This commit is contained in:
2026-08-24 23:05:54 +08:00
parent a207928d39
commit f103a01857
3 changed files with 524 additions and 107 deletions
@@ -1,4 +1,4 @@
import { Send } from 'lucide-react';
import { ArrowUp } from 'lucide-react';
import type {
ComponentProps,
Dispatch,
@@ -210,14 +210,7 @@ export function ProjectSupervisorView({
type="submit"
disabled={runtimePanelProps.controlBusy || needsUserInput}
>
<Send size={16} aria-hidden="true" />
<span>
{needsUserInput
? '等待回答'
: runtimePanelProps.controlBusy
? '思考中'
: '发送'}
</span>
<ArrowUp size={16} aria-hidden="true" />
</button>
</form>
<small className="project-supervisor-workspace-status">
+348 -13
View File
@@ -5087,7 +5087,6 @@ iframe.preview-frame {
}
.game-workbench-toolbar {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
@@ -5184,9 +5183,6 @@ iframe.preview-frame {
}
.game-workbench-view-actions .game-workbench-play-button {
position: absolute;
left: 50%;
transform: translateX(-50%);
border-color: var(--platform-button-primary-border);
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
@@ -6601,6 +6597,7 @@ iframe.preview-frame {
align-content: start;
gap: 8px;
min-width: 0;
min-height: 156px;
padding: 12px;
border: 1px solid #eadbd4;
border-radius: 12px;
@@ -6744,12 +6741,260 @@ iframe.preview-frame {
font-size: 10px;
}
.game-workbench-chat-avatar {
width: 48px;
height: 48px;
.game-workbench-chat .supervisor-chat-only-runtime-controls {
max-width: none;
}
.game-workbench-chat
.supervisor-chat-only-runtime-controls
.pending-command {
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--platform-surface-border);
border-radius: 10px;
background: var(--platform-warm-bg);
}
.game-workbench-chat
.supervisor-chat-only-runtime-controls
.pending-command
> span {
gap: 4px;
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
color: var(--platform-text-strong);
font-size: 12px;
font-weight: 800;
}
.game-workbench-chat
.supervisor-chat-only-runtime-controls
.pending-command
> span
small {
color: var(--platform-text-soft);
font-weight: 500;
line-height: 1.4;
}
.game-workbench-chat .pending-command-actions button {
min-width: 52px;
height: 34px;
flex: 0 0 auto;
object-fit: contain;
filter: drop-shadow(0 5px 9px rgb(182 98 63 / 16%));
border: 1px solid var(--platform-surface-border);
background: var(--platform-button-secondary-fill);
color: var(--platform-button-secondary-text);
font-size: 11px;
font-weight: 800;
}
.game-workbench-chat .pending-command-actions button:last-child {
border-color: var(--platform-button-primary-border);
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
}
.agent-runtime-status .project-runtime-pending-command > span {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
color: var(--platform-text-strong);
font-size: 12px;
font-weight: 800;
}
.agent-runtime-status .project-runtime-pending-command > span small {
display: block;
margin-top: 4px;
color: var(--platform-text-soft);
font-weight: 500;
line-height: 1.4;
}
.agent-runtime-status .project-runtime-pending-command .pending-command-actions {
display: flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
}
.agent-runtime-status
.project-runtime-pending-command
.pending-command-actions
button:last-child {
border-color: var(--platform-button-primary-border);
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
}
.game-workbench-chat
.agent-runtime-status
.project-runtime-pending-command {
grid-template-columns: minmax(0, 1fr) 136px;
}
.game-workbench-chat
.agent-runtime-status
.project-runtime-pending-command
.pending-command-actions {
display: grid;
grid-template-columns: repeat(2, 64px);
gap: 8px;
width: 136px;
flex: 0 0 136px;
}
.game-workbench-chat
.agent-runtime-status
.project-runtime-pending-command
.pending-command-actions
button {
width: 64px;
min-width: 64px;
max-width: 64px;
padding: 0;
white-space: nowrap;
}
/* Runtime 确认卡在部分项目状态下位于状态包装层外,工作台样式直接绑定卡片自身,避免回落旧的蓝色按钮样式。 */
.game-workbench-chat .project-runtime-pending-command {
display: grid;
grid-template-columns: minmax(0, 1fr) 136px;
align-items: center;
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--platform-surface-border);
border-radius: 10px;
background: var(--platform-warm-bg);
}
.game-workbench-chat .project-supervisor-surface > .project-runtime-pending-command,
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command {
display: grid;
grid-template-columns: minmax(0, 1fr) 136px;
align-items: center;
gap: 12px;
width: 100%;
margin: 0;
padding: 10px 12px;
border: 1px solid var(--platform-surface-border);
border-radius: 10px;
background: var(--platform-warm-bg);
}
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command > span {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
color: var(--platform-text-strong);
font-size: 12px;
font-weight: 800;
}
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command > span small {
display: block;
margin-top: 4px;
color: var(--platform-text-soft);
font-size: 10px;
font-weight: 500;
line-height: 1.4;
}
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command .pending-command-actions {
display: grid;
grid-template-columns: repeat(2, 64px);
gap: 8px;
width: 136px;
flex: 0 0 136px;
}
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command .pending-command-actions button {
width: 64px;
min-width: 64px;
max-width: 64px;
height: 34px;
padding: 0;
border: 1px solid var(--platform-surface-border);
border-radius: 8px;
background: var(--platform-button-secondary-fill);
color: var(--platform-button-secondary-text);
font-size: 11px;
font-weight: 800;
white-space: nowrap;
}
.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command .pending-command-actions button:last-child {
border-color: var(--platform-button-primary-border);
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
}
/* 该卡片的两个操作按钮禁止被全局 button 规则压缩,确保“拒绝 / 确认”完整可见。 */
.project-runtime-pending-command .pending-command-actions button {
display: inline-flex !important;
width: 64px !important;
min-width: 64px !important;
max-width: 64px !important;
flex: 0 0 64px !important;
align-items: center;
justify-content: center;
padding: 0 !important;
overflow: visible;
white-space: nowrap;
}
.game-workbench-chat .project-runtime-pending-command > span {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
color: var(--platform-text-strong);
font-size: 12px;
font-weight: 800;
}
.game-workbench-chat .project-runtime-pending-command > span small {
display: block;
margin-top: 4px;
color: var(--platform-text-soft);
font-size: 10px;
font-weight: 500;
line-height: 1.4;
}
.game-workbench-chat .project-runtime-pending-command .pending-command-actions {
display: grid;
grid-template-columns: repeat(2, 64px);
gap: 8px;
width: 136px;
flex: 0 0 136px;
}
.game-workbench-chat
.project-runtime-pending-command
.pending-command-actions
button {
width: 64px;
min-width: 64px;
max-width: 64px;
height: 34px;
padding: 0;
border: 1px solid var(--platform-surface-border);
border-radius: 8px;
background: var(--platform-button-secondary-fill);
color: var(--platform-button-secondary-text);
font-size: 11px;
font-weight: 800;
white-space: nowrap;
}
.game-workbench-chat
.project-runtime-pending-command
.pending-command-actions
button:last-child {
border-color: var(--platform-button-primary-border);
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
}
.game-workbench-chat .project-supervisor-surface {
@@ -6971,6 +7216,100 @@ iframe.preview-frame {
white-space: nowrap;
}
/* 右侧对话采用简洁的 Codex 式消息流与编辑框,减少卡片嵌套和视觉噪音。 */
.game-workbench-chat .project-supervisor-message-list {
position: absolute;
top: 0;
right: 0;
bottom: 104px;
left: 0;
gap: 34px;
height: auto;
padding: 18px 20px 24px;
scroll-padding-bottom: 24px;
border: 1px solid var(--platform-subpanel-border);
border-radius: 12px;
background: var(--platform-input-fill);
}
.game-workbench-chat .project-supervisor-message-list .message {
display: block;
width: 100%;
max-width: 100%;
padding: 4px 0;
border: 0;
border-radius: 0;
background: transparent;
color: var(--platform-text-base);
font-size: 14px;
line-height: 1.85;
}
.game-workbench-chat .project-supervisor-message-list .message + .message {
margin-top: 0;
}
.game-workbench-chat .project-supervisor-message-list .message--user {
width: fit-content;
max-width: min(82%, 640px);
margin-left: auto;
padding: 11px 16px;
border: 1px solid var(--platform-button-primary-border);
border-radius: 14px 14px 4px;
background: var(--platform-warm-bg);
color: var(--platform-text-strong);
}
.game-workbench-chat .project-supervisor-composer {
display: block;
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 5;
padding: 0;
pointer-events: none;
}
.game-workbench-chat .project-supervisor-composer textarea {
display: block;
width: 100%;
min-height: 88px;
max-height: 180px;
padding: 14px 48px 38px 15px;
border: 1px solid var(--platform-surface-border);
border-radius: 14px;
background: var(--platform-input-fill);
color: var(--platform-text-strong);
font-size: 13px;
line-height: 1.6;
resize: vertical;
pointer-events: auto;
}
.game-workbench-chat .project-supervisor-composer button {
position: absolute;
right: 10px;
bottom: 10px;
display: grid;
width: 30px;
min-width: 30px;
height: 30px;
min-height: 30px;
padding: 0;
border: 0;
border-radius: 9px;
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
place-items: center;
pointer-events: auto;
}
.game-workbench-chat .project-supervisor-composer button svg {
width: 14px;
height: 14px;
}
.game-agent-dock {
position: relative;
z-index: 30;
@@ -7382,10 +7721,6 @@ iframe.preview-frame {
justify-content: flex-end;
}
.game-workbench-view-actions .game-workbench-play-button {
position: static;
transform: none;
}
}
@media (max-width: 760px) {
@@ -45,7 +45,6 @@ import type {
GameIterationVersion,
ProjectResourceCanvasLayoutMode,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import PRODUCT_LOGO from '../../../../../packages/shared/src/icons/taonier-product-ip.png';
import { canonicalAssetBaseName } from '../../features/asset-canvas/assetCanvasNaming';
import {
type AssetCanvasCommitNotification,
@@ -833,6 +832,15 @@ export default function ProjectDevelopmentView({
useState<ResourceCanvasViewportBySortMode>(defaultResourceCanvasViewports);
const [activeResourceCategory, setActiveResourceCategory] =
useState<ResourceCategory | null>(null);
const [unreadResourceCategoryState, setUnreadResourceCategoryState] =
useState<{
scopeKey: string;
categories: Set<ResourceCategory>;
}>(() => ({ scopeKey: '', categories: new Set() }));
const resourceCategorySnapshotRef = useRef<{
scopeKey: string;
resourceIdsByCategory: Map<ResourceCategory, Set<string>>;
} | null>(null);
const [selectedResourceId, setSelectedResourceId] = useState<string | null>(
null,
);
@@ -872,8 +880,10 @@ export default function ProjectDevelopmentView({
const [hiddenCommittedResourceId, setHiddenCommittedResourceId] = useState<
string | null
>(null);
const [resourcePreviewVersionByResourceId, setResourcePreviewVersionByResourceId] =
useState<Map<string, string>>(() => new Map());
const [
resourcePreviewVersionByResourceId,
setResourcePreviewVersionByResourceId,
] = useState<Map<string, string>>(() => new Map());
const [approvalMode, setApprovalMode] = useState<ApprovalMode>('strict');
const [approvalDialogOpen, setApprovalDialogOpen] = useState(false);
const [approvalNotice, setApprovalNotice] = useState('');
@@ -1375,6 +1385,24 @@ export default function ProjectDevelopmentView({
),
[resources],
);
const resourceIdsByCategory = useMemo(
() =>
new Map(
categoryOrder.map((category) => [
category,
new Set(
(projectResourcesByCategory.get(category) ?? []).map(
(resource) => resource.id,
),
),
]),
),
[projectResourcesByCategory],
);
const resourceCategoryScopeKey = useMemo(
() => JSON.stringify([projectPath, manifest.projectId]),
[manifest.projectId, projectPath],
);
const resourcePageCategories = useMemo(
() => (resources.length > 0 ? categoryOrder : []),
[resources.length],
@@ -1388,6 +1416,63 @@ export default function ProjectDevelopmentView({
) ??
resourcePageCategories[0] ??
null;
const viewedResourceCategory =
mode === 'resources' ? activePageCategory : null;
useEffect(() => {
const previousSnapshot = resourceCategorySnapshotRef.current;
if (
!previousSnapshot ||
previousSnapshot.scopeKey !== resourceCategoryScopeKey
) {
resourceCategorySnapshotRef.current = {
scopeKey: resourceCategoryScopeKey,
resourceIdsByCategory,
};
setUnreadResourceCategoryState({
scopeKey: resourceCategoryScopeKey,
categories: new Set(),
});
return;
}
const categoriesWithNewResources = categoryOrder.filter((category) => {
if (category === viewedResourceCategory) {
return false;
}
const previousIds =
previousSnapshot.resourceIdsByCategory.get(category) ?? new Set();
return Array.from(resourceIdsByCategory.get(category) ?? []).some(
(resourceId) => !previousIds.has(resourceId),
);
});
resourceCategorySnapshotRef.current = {
scopeKey: resourceCategoryScopeKey,
resourceIdsByCategory,
};
setUnreadResourceCategoryState((current) => {
const categories =
current.scopeKey === resourceCategoryScopeKey
? new Set(current.categories)
: new Set<ResourceCategory>();
let changed = current.scopeKey !== resourceCategoryScopeKey;
if (viewedResourceCategory && categories.delete(viewedResourceCategory)) {
changed = true;
}
categoriesWithNewResources.forEach((category) => {
if (!categories.has(category)) {
categories.add(category);
changed = true;
}
});
return changed
? { scopeKey: resourceCategoryScopeKey, categories }
: current;
});
}, [resourceCategoryScopeKey, resourceIdsByCategory, viewedResourceCategory]);
const unreadResourceCategories =
unreadResourceCategoryState.scopeKey === resourceCategoryScopeKey
? unreadResourceCategoryState.categories
: new Set<ResourceCategory>();
resourceCanvasPageWheelRef.current.activeCategory = activePageCategory;
const nextPageCategory =
activePageCategory && resourcePageCategories.length > 1
@@ -1502,8 +1587,7 @@ export default function ProjectDevelopmentView({
: { x: 48, y: 48, scale: 1 };
const setResourceCanvasViewport = useCallback(
(candidate: CanvasViewport) => {
const activeCategory =
resourceCanvasPageWheelRef.current.activeCategory;
const activeCategory = resourceCanvasPageWheelRef.current.activeCategory;
if (!activeCategory) {
return;
}
@@ -1787,8 +1871,7 @@ export default function ProjectDevelopmentView({
const advanceResourceCanvasPage = useCallback(
(stepOffset: number) => {
const currentCategory =
resourceCanvasPageWheelRef.current.activeCategory;
const currentCategory = resourceCanvasPageWheelRef.current.activeCategory;
const normalizedStepOffset = Math.trunc(stepOffset);
if (
!currentCategory ||
@@ -1836,8 +1919,7 @@ export default function ProjectDevelopmentView({
return;
}
const now = performance.now();
const waitMs =
pageWheelSwitchIntervalMs - (now - state.lastSwitchAt);
const waitMs = pageWheelSwitchIntervalMs - (now - state.lastSwitchAt);
if (waitMs > 0) {
if (state.pendingTimerId === null) {
state.pendingTimerId = window.setTimeout(() => {
@@ -1912,8 +1994,7 @@ export default function ProjectDevelopmentView({
}
pageWheelState.accumulatedDeltaY += normalizedDeltaY;
if (
Math.abs(pageWheelState.accumulatedDeltaY) >=
pageWheelDeltaThreshold
Math.abs(pageWheelState.accumulatedDeltaY) >= pageWheelDeltaThreshold
) {
const stepCount = Math.max(
1,
@@ -1939,7 +2020,8 @@ export default function ProjectDevelopmentView({
}
const viewport = resolveViewport(event);
const category = viewport?.dataset.resourceSectionScroll as
ResourceCategory | undefined;
| ResourceCategory
| undefined;
if (!viewport || !category) {
return;
}
@@ -1958,7 +2040,8 @@ export default function ProjectDevelopmentView({
const event = rawEvent as WebKitGestureEvent;
const viewport = resolveViewport(event);
const category = viewport?.dataset.resourceSectionScroll as
ResourceCategory | undefined;
| ResourceCategory
| undefined;
if (!viewport || !category) {
return;
}
@@ -2048,11 +2131,7 @@ export default function ProjectDevelopmentView({
]);
useLayoutEffect(() => {
if (
mode !== 'resources' ||
!activePageCategory ||
!activePageLayoutReady
) {
if (mode !== 'resources' || !activePageCategory || !activePageLayoutReady) {
return undefined;
}
const canvas = resourceCanvasRef.current;
@@ -2113,7 +2192,8 @@ export default function ProjectDevelopmentView({
.querySelectorAll<HTMLElement>('[data-resource-section-scroll]')
.forEach((viewport) => {
const category = viewport.dataset.resourceSectionScroll as
ResourceCategory | undefined;
| ResourceCategory
| undefined;
if (!category) {
return;
}
@@ -2133,7 +2213,8 @@ export default function ProjectDevelopmentView({
?.querySelectorAll<HTMLElement>('[data-resource-section-scroll]')
.forEach((viewport) => {
const category = viewport.dataset.resourceSectionScroll as
ResourceCategory | undefined;
| ResourceCategory
| undefined;
if (!category) {
return;
}
@@ -2735,9 +2816,7 @@ export default function ProjectDevelopmentView({
}
drag.changed = true;
const coordinate = (start: number, delta: number) =>
normalizeResourceCanvasDragCoordinate(
start + delta / drag.startScale,
);
normalizeResourceCanvasDragCoordinate(start + delta / drag.startScale);
const preview = {
resourceId: drag.resourceId,
x: coordinate(drag.startX, deltaX),
@@ -3776,16 +3855,16 @@ export default function ProjectDevelopmentView({
dependencyLayoutSettled: dependencyLayout.settled,
dependencyPositioned: Boolean(
intent.resourceId &&
dependencyLayout.layout.positions.some(
(position) => position.resourceId === intent.resourceId,
),
dependencyLayout.layout.positions.some(
(position) => position.resourceId === intent.resourceId,
),
),
typeLayoutSettled: typeLayout.settled,
typePositioned: Boolean(
intent.resourceId &&
typeLayout.layout.positions.some(
(position) => position.resourceId === intent.resourceId,
),
typeLayout.layout.positions.some(
(position) => position.resourceId === intent.resourceId,
),
),
visible: targetVisible,
domRendered: Boolean(card),
@@ -3925,27 +4004,37 @@ export default function ProjectDevelopmentView({
data-unavailable={!runAvailable || uiEditorRoute || undefined}
className={mode === 'run' ? 'is-active' : ''}
disabled={uiEditorRoute !== null}
onClick={showRunView}
onClick={() => {
if (!runAvailable) {
return;
}
showRunView();
onPlay?.();
}}
>
</button>
</div>
<div className="game-workbench-view-actions">
<button
type="button"
className="game-workbench-play-button"
disabled={!runAvailable || !onPlay || uiEditorRoute !== null}
onClick={() => {
showRunView();
onPlay?.();
}}
title={
runAvailable ? '运行当前游戏' : '首个可运行原型完成后才能播放'
}
>
<Play size={15} aria-hidden="true" />
</button>
{mode === 'run' ? (
<button
type="button"
className="game-workbench-play-button"
disabled={!runAvailable || !onPlay || uiEditorRoute !== null}
onClick={() => {
showRunView();
onPlay?.();
}}
title={
runAvailable
? '运行当前游戏'
: '首个可运行原型完成后才能播放'
}
>
<Play size={15} aria-hidden="true" />
</button>
) : null}
{mode === 'resources' &&
!assetCanvasRoute &&
!resourceEditorRoute &&
@@ -3965,15 +4054,6 @@ export default function ProjectDevelopmentView({
: `管理未完成编辑 (${pendingResourceEdits.length})`}
</button>
) : null}
<button
type="button"
className={sortMode === 'dependency' ? 'is-active' : ''}
aria-pressed={sortMode === 'dependency'}
onClick={() => showResourceSortMode('dependency')}
>
<FolderTree size={15} aria-hidden="true" />
</button>
<span
className="game-resource-reorder-status"
role="status"
@@ -3981,15 +4061,32 @@ export default function ProjectDevelopmentView({
>
{resourceLayoutSaving ? '保存中' : resourceLayoutNotice}
</span>
<button
type="button"
className={sortMode === 'type' ? 'is-active' : ''}
aria-pressed={sortMode === 'type'}
onClick={() => showResourceSortMode('type')}
<div
className="game-workbench-tabs game-resource-sort-tabs"
role="group"
aria-label="资源排列方式"
>
<ListFilter size={15} aria-hidden="true" />
</button>
<button
type="button"
className={sortMode === 'dependency' ? 'is-active' : ''}
aria-label="按依赖"
aria-pressed={sortMode === 'dependency'}
onClick={() => showResourceSortMode('dependency')}
>
<FolderTree size={15} aria-hidden="true" />
</button>
<button
type="button"
className={sortMode === 'type' ? 'is-active' : ''}
aria-label="按类型"
aria-pressed={sortMode === 'type'}
onClick={() => showResourceSortMode('type')}
>
<ListFilter size={15} aria-hidden="true" />
</button>
</div>
</>
) : null}
</div>
@@ -4479,10 +4576,15 @@ export default function ProjectDevelopmentView({
>
{resourcePageCategories.map((category) => {
const Icon = categoryIcons[category];
const hasUnreadResources =
unreadResourceCategories.has(category);
return (
<button
key={category}
type="button"
aria-label={`${categoryLabels[category]}${
hasUnreadResources ? ',有新资源' : ''
}`}
aria-current={
category === activePageCategory
? 'page'
@@ -4498,7 +4600,15 @@ export default function ProjectDevelopmentView({
}
>
<Icon size={14} aria-hidden="true" />
<strong>{categoryLabels[category]}</strong>
<strong className="game-resource-outline-label">
{categoryLabels[category]}
{hasUnreadResources ? (
<span
className="game-resource-outline-unread"
aria-hidden="true"
/>
) : null}
</strong>
</button>
);
})}
@@ -4760,29 +4870,13 @@ export default function ProjectDevelopmentView({
<dd>{selectedResource.mediaType}</dd>
</div>
</dl>
) : (
<p></p>
)}
) : null}
</section>
<section aria-label="数值微调面板">
<header>
<SlidersHorizontal size={16} aria-hidden="true" />
</header>
<label>
<input type="number" disabled placeholder="未载入" />
</label>
<label>
<input type="number" disabled placeholder="未载入" />
</label>
<input
type="text"
disabled
aria-label="新增微调数值项"
placeholder="用自然语言新增微调项(尚未接入)"
/>
</section>
</div>
</section>
@@ -4792,11 +4886,6 @@ export default function ProjectDevelopmentView({
{!uiEditorRoute ? (
<aside className="game-workbench-chat" aria-label="陶泥儿 Agent 对话">
<header>
<img
className="game-workbench-chat-avatar"
src={PRODUCT_LOGO}
alt="陶泥儿"
/>
<div className="game-workbench-chat-title">
<strong></strong>
<small>