统一智能体对话层级并完善素材导出与文档预览
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 2m44s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 2m45s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 2m46s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 2m49s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m53s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m18s
Project CI / Frontend tests (pull_request) Failing after 4m6s
Project CI / Native shell tests (pull_request) Successful in 6m12s
Project CI / Repository checks (pull_request) Failing after 3m48s
Project CI / Backend tests (pull_request) Successful in 7m8s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m59s

当前Agent与策划Agent共用正文和过程表现组件,统一思考及工具调用的小字号浅色样式
素材工具栏按实际动作分组,导出紧邻删除并修复空分组重复分隔线
文档与代码使用共享Markdown预览和代码高亮,复用按需读取与缓存
移除未调用的活跃回合查询命令,保留正式快照恢复链路
补充回归用例并同步技术方案和团队约定
This commit is contained in:
2026-09-16 03:59:27 +08:00
parent d3c117de01
commit 5cd02aa8b0
37 changed files with 1439 additions and 239 deletions
@@ -10,7 +10,10 @@ import {
import { describe, expect, it, vi } from 'vitest';
import type { CanvasLayer } from './ImageCanvasEditorTypes';
import { ImageCanvasSelectedLayerToolbarView } from './ImageCanvasSelectedLayerToolbarView';
import {
ImageCanvasSelectedLayerToolbarView,
type ImageCanvasSelectedToolbarAction,
} from './ImageCanvasSelectedLayerToolbarView';
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
return {
@@ -67,6 +70,121 @@ function renderSelectedToolbar(
}
describe('ImageCanvasSelectedLayerToolbarView', () => {
it('常规动作与末组分别分隔,导出紧邻删除且位于删除之前', () => {
const props = renderSelectedToolbar({
supportedActions: new Set(['quick-edit', 'download']),
downloadLabel: '导出',
extraActions: <button></button>,
endActions: <button></button>,
});
const toolbar = screen.getByRole('toolbar');
expect(
toolbar.querySelectorAll(
'.image-canvas-editor__floating-toolbar-divider',
),
).toHaveLength(2);
expect(
within(toolbar)
.getAllByRole('button')
.map((button) => button.textContent),
).toEqual(['快速编辑', '引用', '导出', '删除素材']);
expect(
screen.getByRole('button', { name: '导出' }).nextElementSibling,
).toBe(screen.getByRole('button', { name: '删除素材' }));
fireEvent.click(screen.getByRole('button', { name: '导出' }));
expect(props.onDownloadLayer).toHaveBeenCalledWith(props.selectedLayer);
});
it('任意动作组合都不生成相邻、开头或结尾分隔线', () => {
const actions: ImageCanvasSelectedToolbarAction[] = [
'quick-edit',
'crop-expand',
'redraw',
'download',
];
for (const mediaType of ['image', 'audio', 'video'] as const) {
for (let mask = 0; mask < 1 << actions.length; mask += 1) {
for (const withExtraActions of [false, true]) {
renderSelectedToolbar({
selectedLayer: createLayer({ mediaType }),
supportedActions: new Set(
actions.filter((_, index) => (mask & (1 << index)) !== 0),
),
extraActions: withExtraActions ? <button></button> : null,
});
const toolbar = screen.getByRole('toolbar');
const divider = '.image-canvas-editor__floating-toolbar-divider';
expect(toolbar.querySelector(`${divider} + ${divider}`)).toBeNull();
expect(toolbar.firstElementChild?.matches(divider) ?? false).toBe(
false,
);
expect(toolbar.lastElementChild?.matches(divider) ?? false).toBe(
false,
);
cleanup();
}
}
}
});
it('空的条件动作和嵌套 Fragment 不产生空分组', () => {
renderSelectedToolbar({
supportedActions: new Set(['quick-edit', 'download']),
extraActions: (
<>
{null}
<>{false}</>
</>
),
endActions: <>{null}</>,
});
const toolbar = screen.getByRole('toolbar');
expect(
toolbar.querySelectorAll(
'.image-canvas-editor__floating-toolbar-divider',
),
).toHaveLength(1);
expect(
within(toolbar)
.getAllByRole('button')
.map((button) => button.getAttribute('aria-label')),
).toEqual(['快速编辑', '下载按钮']);
expect(toolbar.lastElementChild?.tagName).toBe('BUTTON');
});
it.each(['image', 'audio', 'video', 'image-sequence', undefined] as const)(
'%s 素材使用同一个带文字导出入口与回调',
(mediaType) => {
const props = renderSelectedToolbar({
selectedLayer: createLayer({ mediaType }),
supportedActions: new Set(['download']),
downloadLabel: '导出',
extraActions: <button></button>,
endActions: <button></button>,
});
const toolbar = screen.getByRole('toolbar');
expect(
within(toolbar)
.getAllByRole('button')
.map((button) => button.textContent),
).toEqual(['信息', '导出', '删除素材']);
expect(
screen.getByRole('button', { name: '导出' }).nextElementSibling,
).toBe(screen.getByRole('button', { name: '删除素材' }));
fireEvent.click(screen.getByRole('button', { name: '导出' }));
expect(props.onDownloadLayer).toHaveBeenCalledWith(props.selectedLayer);
},
);
it('宿主未放行下载时不展示导出入口', () => {
renderSelectedToolbar({
supportedActions: new Set(),
downloadLabel: '导出',
extraActions: <button></button>,
});
expect(screen.queryByRole('button', { name: '导出' })).toBeNull();
});
it('renders common layer actions in the Lovart toolbar order and forwards callbacks', () => {
const props = renderSelectedToolbar();
const toolbar = screen.getByRole('toolbar', { name: '图片工具栏' });
@@ -10,7 +10,13 @@ import {
Sparkles,
WandSparkles,
} from 'lucide-react';
import type { CSSProperties, ReactNode } from 'react';
import {
Children,
type CSSProperties,
Fragment,
isValidElement,
type ReactNode,
} from 'react';
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
import type { CanvasLayer } from './ImageCanvasEditorTypes';
@@ -39,8 +45,12 @@ type ImageCanvasSelectedLayerToolbarViewProps = {
* 让没有对应后端链路的画布宿主不必把「点了没反应」的按钮渲染出来。
*/
supportedActions?: ReadonlySet<ImageCanvasSelectedToolbarAction> | null;
/** 宿主在工具条上追加的动作节点,渲染在下载按钮之前。 */
/** 宿主在工具条上追加的常规动作,渲染在下载按钮之前。 */
extraActions?: ReactNode;
/** 工具栏末组动作,紧随下载按钮,与下载共用一个分组。 */
endActions?: ReactNode;
/** 提供文字时显示带文字的导出入口;缺省保持网页画布的图标按钮。 */
downloadLabel?: string;
selectedLayer: CanvasLayer | null;
selectedToolbarStyle: CSSProperties | null;
onOpenQuickEditPanel: (layer: CanvasLayer) => void;
@@ -58,9 +68,20 @@ type ImageCanvasSelectedLayerToolbarViewProps = {
onDownloadLayer: (layer: CanvasLayer) => void;
};
/** 条件动作通常放在 Fragment 中;空 Fragment 不能产生一个空分组。 */
function hasToolbarActions(actions: ReactNode): boolean {
return Children.toArray(actions).some((action) =>
isValidElement<{ children?: ReactNode }>(action) && action.type === Fragment
? hasToolbarActions(action.props.children)
: action !== '',
);
}
export function ImageCanvasSelectedLayerToolbarView({
supportedActions = null,
extraActions,
endActions,
downloadLabel,
selectedLayer,
selectedToolbarStyle,
onOpenQuickEditPanel,
@@ -88,12 +109,35 @@ export function ImageCanvasSelectedLayerToolbarView({
'redraw',
canOpenRedrawPanel(selectedLayer),
);
const extraActionDivider = extraActions ? (
const showDownload = isActionSupported('download', true);
const downloadAction = showDownload ? (
downloadLabel ? (
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label={downloadLabel}
title={downloadLabel}
icon={<Download className="h-4 w-4" />}
onClick={() => onDownloadLayer(selectedLayer)}
>
<span>{downloadLabel}</span>
</CanvasChromeButton>
) : (
<EditorIconButton
label="下载按钮"
title="下载按钮"
icon={Download}
onClick={() => onDownloadLayer(selectedLayer)}
/>
)
) : null;
const hasExtraActions = hasToolbarActions(extraActions);
const hasEndActions = showDownload || hasToolbarActions(endActions);
const divider = (
<span
aria-hidden="true"
className="image-canvas-editor__floating-toolbar-divider"
/>
) : null;
);
if (selectedLayer.mediaType === 'audio') {
return (
@@ -115,16 +159,11 @@ export function ImageCanvasSelectedLayerToolbarView({
<span></span>
</CanvasChromeButton>
) : null}
{extraActionDivider}
{canRedraw && hasExtraActions ? divider : null}
{extraActions}
{isActionSupported('download', true) ? (
<EditorIconButton
label="下载按钮"
title="下载按钮"
icon={Download}
onClick={() => onDownloadLayer(selectedLayer)}
/>
) : null}
{(canRedraw || hasExtraActions) && hasEndActions ? divider : null}
{downloadAction}
{endActions}
</div>
);
}
@@ -141,6 +180,25 @@ export function ImageCanvasSelectedLayerToolbarView({
'character-animation',
selectedLayer.assetKind === 'character',
);
const showCropExpand =
canRasterEdit && isActionSupported('crop-expand', true);
const showRemoveBackground =
canRasterEdit && isActionSupported('remove-background', true);
const showPerfectPixel =
canRasterEdit && isActionSupported('perfect-pixel', true);
const showSplitIconSpritesheet =
selectedLayer.assetKind === 'icon-spritesheet' &&
isActionSupported('split-icon-spritesheet', true);
const showExtractUiDesign =
selectedLayer.assetKind === 'ui-design' &&
isActionSupported('extract-ui-design', true);
const hasEditingActions =
showCropExpand ||
showRemoveBackground ||
showPerfectPixel ||
showSplitIconSpritesheet ||
showExtractUiDesign ||
showCharacterAnimation;
return (
<div
@@ -151,23 +209,18 @@ export function ImageCanvasSelectedLayerToolbarView({
onPointerDown={(event) => event.stopPropagation()}
>
{showQuickEdit ? (
<>
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label="快速编辑"
title="快速编辑"
icon={<Sparkles className="h-4 w-4" />}
onClick={() => onOpenQuickEditPanel(selectedLayer)}
>
<span></span>
</CanvasChromeButton>
<span
aria-hidden="true"
className="image-canvas-editor__floating-toolbar-divider"
/>
</>
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label="快速编辑"
title="快速编辑"
icon={<Sparkles className="h-4 w-4" />}
onClick={() => onOpenQuickEditPanel(selectedLayer)}
>
<span></span>
</CanvasChromeButton>
) : null}
{canRasterEdit && isActionSupported('crop-expand', true) ? (
{showQuickEdit && hasEditingActions ? divider : null}
{showCropExpand ? (
<EditorIconButton
label="裁扩按钮"
title="裁扩按钮"
@@ -175,7 +228,7 @@ export function ImageCanvasSelectedLayerToolbarView({
onClick={() => onOpenCropExpandPanel(selectedLayer)}
/>
) : null}
{canRasterEdit && isActionSupported('remove-background', true) ? (
{showRemoveBackground ? (
<EditorIconButton
label="去除背景按钮"
title="去除背景按钮"
@@ -183,7 +236,7 @@ export function ImageCanvasSelectedLayerToolbarView({
onClick={() => onRemoveBackground(selectedLayer)}
/>
) : null}
{canRasterEdit && isActionSupported('perfect-pixel', true) ? (
{showPerfectPixel ? (
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
// 中文注释:保存态名称不能直接用「素材类型保存中」——icon-spritesheet 图层上
@@ -236,8 +289,7 @@ export function ImageCanvasSelectedLayerToolbarView({
</span>
</CanvasChromeButton>
) : null}
{selectedLayer.assetKind === 'icon-spritesheet' &&
isActionSupported('split-icon-spritesheet', true) ? (
{showSplitIconSpritesheet ? (
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label={
@@ -274,8 +326,7 @@ export function ImageCanvasSelectedLayerToolbarView({
</span>
</CanvasChromeButton>
) : null}
{selectedLayer.assetKind === 'ui-design' &&
isActionSupported('extract-ui-design', true) ? (
{showExtractUiDesign ? (
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label="提取素材"
@@ -299,10 +350,7 @@ export function ImageCanvasSelectedLayerToolbarView({
) : null}
{canRedraw ? (
<>
<span
aria-hidden="true"
className="image-canvas-editor__floating-toolbar-divider"
/>
{showQuickEdit || hasEditingActions ? divider : null}
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label="改造"
@@ -314,16 +362,16 @@ export function ImageCanvasSelectedLayerToolbarView({
</CanvasChromeButton>
</>
) : null}
{extraActionDivider}
{(showQuickEdit || hasEditingActions || canRedraw) && hasExtraActions
? divider
: null}
{extraActions}
{isActionSupported('download', true) ? (
<EditorIconButton
label="下载按钮"
title="下载按钮"
icon={Download}
onClick={() => onDownloadLayer(selectedLayer)}
/>
) : null}
{(showQuickEdit || hasEditingActions || canRedraw || hasExtraActions) &&
hasEndActions
? divider
: null}
{downloadAction}
{endActions}
</div>
);
}