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

@@ -483,7 +483,30 @@ test('运行态会换签并渲染抓大鹅中心容器 UI 图', async () => {
);
});
test('容器图换签失败时保留默认圆形容器兜底', async () => {
test('运行态没有生成容器时使用透明参考容器兜底', async () => {
const run = startLocalMatch3DRun(3);
renderRuntime(run, []);
let containerImage!: HTMLImageElement;
await waitFor(() => {
containerImage = screen.getByTestId(
'match3d-container-image',
) as HTMLImageElement;
expect(containerImage.getAttribute('src')).toBe(
'/match3d-background-references/pot-fused-reference.png',
);
});
fireEvent.load(containerImage);
expect(screen.getByTestId('match3d-board').className).toContain(
'bg-transparent',
);
expect(screen.getByTestId('match3d-board').className).not.toContain(
'rounded-full',
);
});
test('容器图换签失败时使用透明参考容器兜底', async () => {
const run = startLocalMatch3DRun(3);
const generatedItemAssets: Match3DGeneratedItemAsset[] = [
{
@@ -515,12 +538,17 @@ test('容器图换签失败时保留默认圆形容器兜底', async () => {
await waitFor(() => {
expect(globalThis.fetch).toHaveBeenCalled();
});
expect(screen.queryByTestId('match3d-container-image')).toBeNull();
await waitFor(() => {
expect(
screen.getByTestId('match3d-container-image').getAttribute('src'),
).toBe('/match3d-background-references/pot-fused-reference.png');
});
fireEvent.load(screen.getByTestId('match3d-container-image'));
expect(screen.getByTestId('match3d-board').className).toContain(
'rounded-full',
'bg-transparent',
);
expect(screen.getByTestId('match3d-board').className).not.toContain(
'bg-transparent',
'rounded-full',
);
});

View File

@@ -108,6 +108,8 @@ function resolveTrayPreviewItem(
}
const DEFAULT_MATCH3D_MUSIC_VOLUME = 0.42;
const MATCH3D_CONTAINER_REFERENCE_SRC =
'/match3d-background-references/pot-fused-reference.png';
function formatTimer(value: number) {
const totalSeconds = Math.max(0, Math.ceil(value / 1000));
@@ -580,7 +582,7 @@ export function Match3DRuntimeShell({
)
.find(Boolean) ||
'';
const containerAssetSrc =
const generatedContainerAssetSrc =
generatedBackgroundAsset?.containerImageSrc?.trim() ||
generatedBackgroundAsset?.containerImageObjectKey?.trim() ||
runtimeGeneratedItemAssets
@@ -591,6 +593,8 @@ export function Match3DRuntimeShell({
'',
)
.find(Boolean) || '';
const containerAssetSrc =
generatedContainerAssetSrc || MATCH3D_CONTAINER_REFERENCE_SRC;
const imageSourcesByType = useMemo(
() => buildMatch3DImageSourcesByType(run, runtimeGeneratedItemAssets),
[runtimeGeneratedItemAssets, run],
@@ -726,12 +730,6 @@ export function Match3DRuntimeShell({
}, [backgroundAssetSrc]);
useEffect(() => {
if (!containerAssetSrc) {
setResolvedContainerImageSrc('');
setIsContainerImageLoaded(false);
return undefined;
}
let cancelled = false;
const controller = new AbortController();
setResolvedContainerImageSrc('');
@@ -748,7 +746,11 @@ export function Match3DRuntimeShell({
})
.catch(() => {
if (!cancelled) {
setResolvedContainerImageSrc('');
setResolvedContainerImageSrc(
containerAssetSrc === MATCH3D_CONTAINER_REFERENCE_SRC
? ''
: MATCH3D_CONTAINER_REFERENCE_SRC,
);
setIsContainerImageLoaded(false);
}
});
@@ -911,7 +913,7 @@ export function Match3DRuntimeShell({
style={{
boxSizing: 'border-box',
maxWidth: '100vw',
width: 'min(100vw, 23.5rem)',
width: 'min(100vw, 28rem)',
}}
>
<header className="flex items-center justify-between gap-2">
@@ -946,7 +948,7 @@ export function Match3DRuntimeShell({
: 'overflow-hidden rounded-full border-[10px] border-[#e6d19b] bg-[radial-gradient(circle_at_50%_42%,#f2d993_0%,#c88f43_56%,#835223_100%)] shadow-[inset_0_8px_34px_rgba(72,41,16,0.34),0_22px_42px_rgba(15,23,42,0.28)]'
}`}
style={{
width: 'min(92vw, 58dvh, 100%)',
width: 'min(96vw, 60dvh, 100%)',
}}
onPointerDown={handleBoardPointerDown}
data-testid="match3d-board"
@@ -956,14 +958,18 @@ export function Match3DRuntimeShell({
src={resolvedContainerImageSrc}
alt=""
aria-hidden="true"
className={`pointer-events-none absolute inset-[-8%] z-0 h-[116%] w-[116%] object-contain drop-shadow-[0_22px_42px_rgba(15,23,42,0.28)] ${
className={`pointer-events-none absolute inset-[-10%] z-0 h-[120%] w-[120%] object-contain drop-shadow-[0_22px_42px_rgba(15,23,42,0.28)] ${
isContainerImageLoaded ? 'opacity-100' : 'opacity-0'
}`}
data-testid="match3d-container-image"
onLoad={() => setIsContainerImageLoaded(true)}
onError={() => {
setIsContainerImageLoaded(false);
setResolvedContainerImageSrc('');
setResolvedContainerImageSrc((currentSrc) =>
currentSrc && currentSrc !== MATCH3D_CONTAINER_REFERENCE_SRC
? MATCH3D_CONTAINER_REFERENCE_SRC
: '',
);
}}
/>
) : (