Increase VectorEngine timeouts and add image UI

Add VectorEngine image generation config and raise request timeouts (env + scripts) from 180000 to 1000000ms. Introduce a reusable CreativeImageInputPanel component with tests and wire up mobile keyboard-focus helpers; update generation views and related tests (CustomWorldGenerationView, BarkBattle editor, Match3D, Puzzle flows). Improve API error handling / VectorEngine request guidance (packages/shared http.ts and docs), and apply multiple backend/frontend fixes for puzzle/match3d/prompt handling. Also include extensive docs and decision-log updates describing UI/UX decisions and verification steps.
This commit is contained in:
2026-05-15 02:40:59 +08:00
parent 4642855fd0
commit 74fd9a33ac
87 changed files with 5508 additions and 1261 deletions

View File

@@ -189,6 +189,7 @@ test('puzzle workspace submits the work form instead of agent chat', () => {
seedText: '一只猫在雨夜灯牌下回头。',
pictureDescription: '一只猫在雨夜灯牌下回头。',
referenceImageSrc: null,
referenceImageSrcs: [],
imageModel: 'gpt-image-2',
aiRedraw: true,
});
@@ -273,8 +274,8 @@ test('puzzle workspace selects a history image from the upload card', async () =
ownerLabel: '账号 user-1',
profileId: null,
entityId: 'puzzle-session-1',
createdAt: '2026-04-27T10:00:00.000Z',
updatedAt: '2026-04-27T10:00:00.000Z',
createdAt: '1713686400.000000Z',
updatedAt: '1713686400.000000Z',
},
]);
@@ -299,8 +300,11 @@ test('puzzle workspace selects a history image from the upload card', async () =
const picker = await screen.findByRole('dialog', {
name: '选择历史图片',
});
expect(await within(picker).findByText('image.png')).toBeTruthy();
expect(await within(picker).findByText(/2024\/04\/21/u)).toBeTruthy();
expect(within(picker).queryByText('账号 user-1')).toBeNull();
fireEvent.click(
await within(picker).findByRole('button', { name: / user-1/u }),
await within(picker).findByRole('button', { name: /image\.png/u }),
);
await waitFor(() => {
@@ -310,6 +314,7 @@ test('puzzle workspace selects a history image from the upload card', async () =
'src',
expect.stringContaining('/generated-puzzle-assets/history/image.png'),
);
expect(screen.getByLabelText('画面AI重绘要求提示词')).toBeTruthy();
fireEvent.change(screen.getByLabelText('画面AI重绘要求提示词'), {
target: { value: '保留历史图里的主体,改成晴天花园。' },
@@ -321,6 +326,7 @@ test('puzzle workspace selects a history image from the upload card', async () =
seedText: '保留历史图里的主体,改成晴天花园。',
pictureDescription: '保留历史图里的主体,改成晴天花园。',
referenceImageSrc: '/generated-puzzle-assets/history/image.png',
referenceImageSrcs: [],
imageModel: 'gpt-image-2',
aiRedraw: true,
});
@@ -377,6 +383,7 @@ test('puzzle workspace falls back to compile action for restored sessions', () =
pictureDescription: '潮雾中的灯塔与断桥',
promptText: '潮雾中的灯塔与断桥',
referenceImageSrc: null,
referenceImageSrcs: [],
imageModel: 'gpt-image-2',
aiRedraw: true,
candidateCount: 1,
@@ -476,6 +483,7 @@ test('puzzle workspace restores form draft fields and autosaves edits', () => {
seedText: '旧街灯牌下的猫和发光雨伞。',
pictureDescription: '旧街灯牌下的猫和发光雨伞。',
referenceImageSrc: null,
referenceImageSrcs: [],
imageModel: 'gpt-image-2',
aiRedraw: true,
});
@@ -521,6 +529,7 @@ test('puzzle workspace hides prompt and cost when AI redraw is off', async () =>
seedText: 'first-level.png',
pictureDescription: 'first-level.png',
referenceImageSrc: uploadedDataUrl,
referenceImageSrcs: [],
imageModel: 'gpt-image-2',
aiRedraw: false,
});
@@ -559,6 +568,90 @@ test('puzzle workspace submits uploaded reference image when AI redraw is on', a
seedText: '保留上传画面的主体和构图,改成雨夜灯街。',
pictureDescription: '保留上传画面的主体和构图,改成雨夜灯街。',
referenceImageSrc: uploadedDataUrl,
referenceImageSrcs: [],
imageModel: 'gpt-image-2',
aiRedraw: true,
});
});
test('puzzle workspace uploads prompt reference images from the description box', async () => {
const onCreateFromForm = vi.fn();
const uploadedSources = [
'data:image/png;base64,reference-1',
'data:image/png;base64,reference-2',
'data:image/png;base64,reference-3',
'data:image/png;base64,reference-4',
'data:image/png;base64,reference-5',
'data:image/png;base64,reference-6',
];
let readIndex = 0;
const firstUploadedSource = uploadedSources[0] || 'data:image/png;base64,reference-1';
stubReferenceImageUpload(firstUploadedSource);
class MockFileReader {
result: string | null = null;
onload: null | (() => void) = null;
onerror: null | (() => void) = null;
readAsDataURL() {
this.result =
uploadedSources[Math.min(readIndex, uploadedSources.length - 1)] ||
firstUploadedSource;
readIndex += 1;
this.onload?.();
}
}
vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader);
render(
<PuzzleAgentWorkspace
session={null}
onBack={() => {}}
onSubmitMessage={() => {}}
onExecuteAction={() => {}}
onCreateFromForm={onCreateFromForm}
/>,
);
fireEvent.change(screen.getByLabelText('画面描述'), {
target: { value: '一只猫在雨夜灯牌下回头。' },
});
fireEvent.change(screen.getByLabelText('上传参考图', { selector: 'input' }), {
target: {
files: uploadedSources.map(
(_source, index) =>
new File(['x'], `reference-${index + 1}.png`, {
type: 'image/png',
}),
),
},
});
await waitFor(() => {
expect(screen.getAllByRole('button', { name: //u })).toHaveLength(
5,
);
});
expect(screen.getByText('参考图最多上传 5 张。')).toBeTruthy();
fireEvent.click(
screen.getByRole('button', { name: / reference-1\.png/u }),
);
expect(
await screen.findByRole('dialog', { name: 'reference-1.png' }),
).toBeTruthy();
expect(screen.getByAltText('参考图预览')).toHaveProperty(
'src',
expect.stringContaining('reference-1'),
);
fireEvent.click(screen.getByRole('button', { name: '关闭参考图预览' }));
fireEvent.click(screen.getByRole('button', { name: /稿/u }));
confirmPuzzlePointCost();
expect(onCreateFromForm).toHaveBeenCalledWith({
seedText: '一只猫在雨夜灯牌下回头。',
pictureDescription: '一只猫在雨夜灯牌下回头。',
referenceImageSrc: null,
referenceImageSrcs: uploadedSources.slice(0, 5),
imageModel: 'gpt-image-2',
aiRedraw: true,
});