prepare info display to user for review

This commit is contained in:
2026-07-10 20:46:27 +08:00
parent ffde522b1e
commit 88368cd900
10 changed files with 751 additions and 129 deletions
@@ -122,13 +122,53 @@ function createPendingToolCallMessage(): EditorAgentMessage {
attachments: [],
toolCall: {
toolName: 'edit-image',
summary: '',
status: 'pending_confirmation',
args: {
object_image_id: 'source-image-1',
reference_image_ids: ['reference-image-1', 'reference-image-2'],
prompt: '把角色换成像素风',
},
displayArgs: {
stringArgs: [
{
name: 'prompt',
label: '提示词',
value: '把角色换成像素风',
},
],
imageArgs: [
{
name: 'object_image_id',
label: '目标图片',
refs: [
{
imageId: 'source-image-1',
imageSrc: 'data:image/png;base64,c291cmNl',
label: '原角色',
width: 512,
height: 512,
},
],
},
{
name: 'reference_image_ids',
label: '参考图片',
refs: [
{
imageId: 'reference-image-1',
imageSrc: 'data:image/png;base64,cmVmZXJlbmNlLTE=',
label: '像素风参考',
},
{
imageId: 'reference-image-2',
imageSrc: 'data:image/png;base64,cmVmZXJlbmNlLTI=',
label: '配色参考',
},
],
},
],
extras: { priceMudPoints: 3 },
},
images: [],
error: null,
},
@@ -500,8 +540,14 @@ describe('EditorAgentConversationPanelView', () => {
);
expect(await screen.findByText('把角色换成像素风')).toBeTruthy();
expect(screen.getByText('source-image-1')).toBeTruthy();
expect(screen.getByText('提示词')).toBeTruthy();
expect(screen.getByText('2 张')).toBeTruthy();
expect(screen.getByAltText('目标图片:原角色')).toBeTruthy();
expect(screen.getByAltText('参考图片:像素风参考')).toBeTruthy();
expect(screen.getByAltText('参考图片:配色参考')).toBeTruthy();
expect(screen.getByText('预计消耗 3泥点')).toBeTruthy();
expect(screen.queryByText('source-image-1')).toBeNull();
expect(screen.queryByText('reference-image-1')).toBeNull();
expect(
screen.queryByText('internal system prompt that must stay hidden'),
).toBeNull();
@@ -511,13 +557,12 @@ describe('EditorAgentConversationPanelView', () => {
expect(client.confirmToolCall).toHaveBeenCalledWith('conversation-1', 2);
});
expect(
(screen.getByRole('button', { name: '执行中' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '取消' }) as HTMLButtonElement)
.disabled,
).toBe(true);
within(screen.getByRole('article', { name: 'Agent操作' })).getByText(
'执行中',
),
).toBeTruthy();
expect(screen.queryByRole('button', { name: '确认' })).toBeNull();
expect(screen.queryByRole('button', { name: '取消' })).toBeNull();
await act(async () => {
resolveConfirmation({
@@ -1,7 +1,8 @@
import { Check, Loader2, Pencil, X } from 'lucide-react';
import { Check, Coins, Loader2, Pencil, X } from 'lucide-react';
import type { EditorAgentToolCall } from '@/packages/shared/src/contracts';
import { editorAgentToolLabel } from '@/src/components/image-editor/EditorAgentConversation/toolCallPresentation.ts';
import { ResolvedAssetImage } from '@/src/components/ResolvedAssetImage.tsx';
type PendingToolCallAction = 'confirm' | 'cancel' | null;
@@ -13,37 +14,10 @@ type PendingToolCallProps = {
onCancel: (messageId: number) => void;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function readString(value: unknown) {
return typeof value === 'string' && value.trim() ? value.trim() : null;
}
function readStringArray(value: unknown) {
return Array.isArray(value)
? value.map(readString).filter((item): item is string => item !== null)
: [];
}
function readPendingToolDetails(toolCall: EditorAgentToolCall) {
const args = isRecord(toolCall.args) ? toolCall.args : {};
const prompt = readString(args.prompt);
const targetImage =
readString(args.object_image_id) ?? readString(args.objectImageId);
const referenceImages = readStringArray(
args.reference_image_ids ?? args.referenceImageIds,
);
const summary = readString(toolCall.summary);
return {
prompt: prompt ?? (summary?.startsWith('{') ? null : summary),
targetImage,
referenceImageCount: referenceImages.length,
};
}
export function PendingToolCall({
messageId,
toolCall,
@@ -51,7 +25,7 @@ export function PendingToolCall({
onConfirm,
onCancel,
}: PendingToolCallProps) {
const details = readPendingToolDetails(toolCall);
const displayArgs = toolCall.displayArgs;
const label = editorAgentToolLabel(toolCall.toolName);
const isBusy = busyAction !== null;
@@ -66,32 +40,66 @@ export function PendingToolCall({
</span>
</div>
{details.prompt ? (
<p className="mt-2 whitespace-pre-wrap break-words text-sm leading-5">
{details.prompt}
</p>
) : null}
<div className="mt-3 space-y-3 empty:hidden">
{displayArgs.stringArgs.map((argument, index) => (
<div
key={`${argument.name}-${index}`}
className="rounded-lg bg-slate-50 px-2.5 py-2"
>
<div className="text-xs font-medium text-slate-500">
{argument.label}
</div>
<div className="mt-1 whitespace-pre-wrap break-words text-sm leading-5 text-slate-800">
{argument.value}
</div>
</div>
))}
{details.targetImage || details.referenceImageCount > 0 ? (
<dl className="mt-2 space-y-1 text-xs text-slate-500">
{details.targetImage ? (
<div className="flex min-w-0 gap-2">
<dt className="shrink-0"></dt>
<dd className="min-w-0 break-all text-slate-700">
{details.targetImage}
</dd>
{displayArgs.imageArgs.map((argument, argumentIndex) => (
<div key={`${argument.name}-${argumentIndex}`}>
<div className="flex items-center gap-2 text-xs font-medium text-slate-500">
<span>{argument.label}</span>
<span className="font-normal text-slate-400">
{argument.refs.length}
</span>
</div>
) : null}
{details.referenceImageCount > 0 ? (
<div className="flex gap-2">
<dt></dt>
<dd className="text-slate-700">
{details.referenceImageCount}
</dd>
</div>
) : null}
</dl>
) : null}
{argument.refs.length ? (
<div className="mt-1.5 flex flex-wrap gap-2">
{argument.refs.map((image, imageIndex) => {
const imageLabel =
readString(image.label) ?? `图片 ${imageIndex + 1}`;
return (
<figure
key={`${image.imageId}-${imageIndex}`}
className="w-20 min-w-0"
>
<div className="overflow-hidden rounded-lg border border-slate-200 bg-slate-100">
<ResolvedAssetImage
src={image.thumbnailSrc ?? image.imageSrc}
objectKey={image.objectKey}
refreshKey={image.imageId}
alt={`${argument.label}${imageLabel}`}
className="h-20 w-20 object-contain"
/>
</div>
{image.label ? (
<figcaption className="mt-1 truncate text-center text-[11px] text-slate-500">
{image.label}
</figcaption>
) : null}
</figure>
);
})}
</div>
) : null}
</div>
))}
</div>
<div className="mt-3 flex items-center gap-1.5 rounded-lg border border-amber-200 bg-amber-50 px-2.5 py-2 text-xs font-medium text-amber-800">
<Coins className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
<span> {displayArgs.extras.priceMudPoints}</span>
</div>
<div className="mt-3 flex justify-end gap-2">
<button
@@ -12,6 +12,26 @@ import {
useEditorAgentConversation,
} from './useEditorAgentConversation.ts';
function createEditImageDisplayArgs(prompt: string) {
return {
stringArgs: [{ name: 'prompt', label: '修改要求', value: prompt }],
imageArgs: [
{
name: 'object_image_id',
label: '目标图片',
refs: [
{
imageId: 'source-image',
imageSrc: '/source-image.png',
label: '源图片',
},
],
},
],
extras: { priceMudPoints: 3 },
};
}
function createClient(): EditorAgentConversationClient {
return {
listConversations: vi.fn().mockResolvedValue([
@@ -61,9 +81,13 @@ function createClient(): EditorAgentConversationClient {
attachments: [],
toolCall: {
toolName: 'generate_image',
summary: '生成像素风格角色',
status: 'completed',
args: {},
displayArgs: {
stringArgs: [],
imageArgs: [],
extras: { priceMudPoints: 0 },
},
images: [
{
resourceId: 'resource-result-1',
@@ -252,12 +276,12 @@ describe('useEditorAgentConversation', () => {
attachments: [],
toolCall: {
toolName: 'edit-image',
summary: '',
status: 'pending_confirmation',
args: {
object_image_id: 'source-image',
prompt: '换成像素风',
},
displayArgs: createEditImageDisplayArgs('换成像素风'),
images: [],
error: null,
},
@@ -273,12 +297,12 @@ describe('useEditorAgentConversation', () => {
attachments: [],
toolCall: {
toolName: 'edit-image',
summary: '',
status: 'completed',
args: {
object_image_id: 'source-image',
prompt: '换成像素风',
},
displayArgs: createEditImageDisplayArgs('换成像素风'),
images: [
{
resourceId: null,
@@ -339,9 +363,9 @@ describe('useEditorAgentConversation', () => {
attachments: [],
toolCall: {
toolName: 'edit-image',
summary: '',
status: 'pending_confirmation',
args: { object_image_id: 'source-image', prompt: '换成像素风' },
displayArgs: createEditImageDisplayArgs('换成像素风'),
images: [],
error: null,
},
@@ -407,9 +431,9 @@ describe('useEditorAgentConversation', () => {
attachments: [],
toolCall: {
toolName: 'edit-image',
summary: '',
status: 'pending_confirmation' as const,
args: { object_image_id: 'source-image', prompt: '换成像素风' },
displayArgs: createEditImageDisplayArgs('换成像素风'),
images: [],
error: null,
},
@@ -69,6 +69,7 @@ const getEditorAgentConversationMock = vi.hoisted(() =>
conversationId: 'editor-agent-conv-test',
projectId: 'editor-project-default',
title: '画布 Agent',
createdAt: '2026-07-03T00:00:00.000Z',
updatedAt: '2026-07-03T00:00:00.000Z',
messages: [],
})),
@@ -176,12 +177,32 @@ function createEditorAgentConversationSummary(
};
}
function createEditImageToolCallDisplayArgs(prompt: string) {
return {
stringArgs: [{ name: 'prompt', label: '修改要求', value: prompt }],
imageArgs: [
{
name: 'object_image_id',
label: '目标图片',
refs: [
{
imageId: 'source-image',
imageSrc: '/source-image.png',
label: '源图片',
},
],
},
],
extras: { priceMudPoints: 3 },
};
}
function createEditorAgentDetailWithGeneration(
resourceId: string,
): EditorAgentConversationDetail {
const summary = createEditorAgentConversationSummary();
const conversationSummary = createEditorAgentConversationSummary();
return {
...summary,
...conversationSummary,
title: '画布 Agent',
createdAt: '2026-07-03T00:00:00.000Z',
messages: [
@@ -192,9 +213,13 @@ function createEditorAgentDetailWithGeneration(
attachments: [],
toolCall: {
toolName: 'generate_image',
summary: '',
status: 'completed',
args: {},
displayArgs: {
stringArgs: [],
imageArgs: [],
extras: { priceMudPoints: 0 },
},
images: [
{
resourceId,
@@ -1855,12 +1880,13 @@ describe('ImageCanvasEditorView', () => {
attachments: [],
toolCall: {
toolName: 'edit-image',
summary: '',
status: 'pending_confirmation',
args: {
object_image_id: 'source-image',
prompt: '把图片换成像素风',
},
displayArgs:
createEditImageToolCallDisplayArgs('把图片换成像素风'),
images: [],
error: null,
},
@@ -1877,9 +1903,10 @@ describe('ImageCanvasEditorView', () => {
attachments: [],
toolCall: {
toolName: 'edit-image',
summary: '',
status: 'completed',
args: {},
displayArgs:
createEditImageToolCallDisplayArgs('把图片换成像素风'),
images: [
{
resourceId: null,
@@ -154,9 +154,13 @@ describe('editorAgentClient', () => {
attachments: [],
toolCall: {
toolName: 'edit-image',
summary: '',
status: 'completed',
args: {},
displayArgs: {
stringArgs: [],
imageArgs: [],
extras: { priceMudPoints: 0 },
},
images: [],
error: null,
},