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

@@ -1,4 +1,6 @@
import { motion } from 'motion/react';
import type { CSSProperties } from 'react';
import { useEffect, useState } from 'react';
import type { CustomWorldGenerationProgress } from '../../packages/shared/src/contracts/runtime';
import type { CustomWorldStructuredAnchorEntry } from '../services/customWorldAgentGenerationProgress';
@@ -24,6 +26,7 @@ interface CustomWorldGenerationViewProps {
pausedBadgeLabel?: string;
idleBadgeLabel?: string;
structuredEmptyText?: string;
hideBatchModule?: boolean;
}
function formatDuration(ms: number) {
@@ -86,6 +89,49 @@ function buildFallbackRenderKey(
return normalizedValue ? normalizedValue : fallback;
}
function useIsMobileGenerationLayout() {
const [isMobile, setIsMobile] = useState(() => {
if (
typeof window === 'undefined' ||
typeof window.matchMedia !== 'function'
) {
return false;
}
return window.matchMedia('(max-width: 639px)').matches;
});
useEffect(() => {
if (
typeof window === 'undefined' ||
typeof window.matchMedia !== 'function'
) {
return undefined;
}
const mediaQuery = window.matchMedia('(max-width: 639px)');
const syncMobileLayout = () => {
setIsMobile(mediaQuery.matches);
};
syncMobileLayout();
if (typeof mediaQuery.addEventListener === 'function') {
mediaQuery.addEventListener('change', syncMobileLayout);
return () => {
mediaQuery.removeEventListener('change', syncMobileLayout);
};
}
mediaQuery.addListener(syncMobileLayout);
return () => {
mediaQuery.removeListener(syncMobileLayout);
};
}, []);
return isMobile;
}
export function CustomWorldGenerationView({
settingText,
anchorEntries = [],
@@ -107,7 +153,9 @@ export function CustomWorldGenerationView({
pausedBadgeLabel = '生成已暂停',
idleBadgeLabel = '等待操作',
structuredEmptyText = '正在整理当前设定结构,请稍后。',
hideBatchModule = false,
}: CustomWorldGenerationViewProps) {
const isMobileGenerationLayout = useIsMobileGenerationLayout();
const progressValue = getProgressPercentage(progress);
const steps = progress?.steps ?? [];
const hasStructuredAnchors = anchorEntries.length > 0;
@@ -116,6 +164,10 @@ export function CustomWorldGenerationView({
const normalizedSettingDescription = settingDescription?.trim() ?? '';
const hasSettingActionLabel = normalizedSettingActionLabel.length > 0;
const hasSettingDescription = normalizedSettingDescription.length > 0;
const shouldHideBatchModule =
hideBatchModule ||
progressTitle === '拼图草稿生成进度' ||
progressTitle === '抓大鹅草稿生成进度';
const estimatedWaitText =
progress?.estimatedRemainingMs != null
? `预计还需 ${formatDuration(progress.estimatedRemainingMs)}`
@@ -179,28 +231,41 @@ export function CustomWorldGenerationView({
/>
</div>
<div className="mt-4 grid gap-2 sm:grid-cols-3 xl:gap-3">
<div className="platform-subpanel rounded-2xl px-4 py-3">
<div className="text-[11px] tracking-[0.16em] text-zinc-500">
<div
className={`custom-world-generation-stats mt-4 grid gap-2 xl:gap-3 ${
shouldHideBatchModule
? 'custom-world-generation-stats--two-column grid-cols-2'
: 'sm:grid-cols-3'
}`}
style={
shouldHideBatchModule
? { gridTemplateColumns: 'repeat(2, minmax(0, 1fr))' }
: undefined
}
>
{shouldHideBatchModule ? null : (
<div className="platform-subpanel min-w-0 rounded-2xl px-4 py-3">
<div className="text-[11px] tracking-[0.16em] text-zinc-500">
</div>
<div className="mt-1 text-sm font-semibold text-white">
{progress?.batchLabel ?? '准备中'}
</div>
</div>
<div className="mt-1 text-sm font-semibold text-white">
{progress?.batchLabel ?? '准备中'}
</div>
</div>
<div className="platform-subpanel rounded-2xl px-4 py-3">
)}
<div className="platform-subpanel min-w-0 rounded-2xl px-3 py-3 sm:px-4">
<div className="text-[11px] tracking-[0.16em] text-zinc-500">
</div>
<div className="mt-1 text-sm font-semibold text-white">
<div className="mt-1 break-keep text-xs font-semibold text-white sm:text-sm">
{estimatedWaitText}
</div>
</div>
<div className="platform-subpanel rounded-2xl px-4 py-3">
<div className="platform-subpanel min-w-0 rounded-2xl px-3 py-3 sm:px-4">
<div className="text-[11px] tracking-[0.16em] text-zinc-500">
</div>
<div className="mt-1 text-sm font-semibold text-white">
<div className="mt-1 break-keep text-xs font-semibold text-white sm:text-sm">
{elapsedText}
</div>
</div>
@@ -211,7 +276,7 @@ export function CustomWorldGenerationView({
const stepProgress = getStepProgressPercentage(step);
return (
<div
<motion.div
key={buildFallbackRenderKey(
step.id,
`progress-step-${index}`,
@@ -222,7 +287,23 @@ export function CustomWorldGenerationView({
: step.status === 'active'
? 'border-sky-300/22 bg-sky-500/10'
: 'platform-subpanel'
}`}
} custom-world-generation-step`}
initial={
isMobileGenerationLayout
? { opacity: 0, x: '-110vw' }
: false
}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.34,
ease: 'easeOut',
delay: isMobileGenerationLayout ? index * 0.09 : 0,
}}
style={
{
'--generation-step-delay': `${index * 90}ms`,
} as CSSProperties
}
>
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 text-sm font-semibold text-white">
@@ -248,7 +329,7 @@ export function CustomWorldGenerationView({
<div className="mt-2 text-xs leading-6 text-zinc-400">
{step.detail}
</div>
</div>
</motion.div>
);
})}
</div>