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,3 +1,4 @@
import { ArrowLeft, Loader2, Trophy, WandSparkles } from 'lucide-react';
import { useMemo, useState } from 'react';
import type { BarkBattleConfigEditorPayload } from '../../../packages/shared/src/contracts/barkBattle';
@@ -6,8 +7,11 @@ import { BarkBattlePreviewCard } from './BarkBattlePreviewCard';
export type BarkBattleConfigEditorProps = {
isBusy?: boolean;
error?: string | null;
onPublish: (payload: BarkBattleConfigEditorPayload) => void | Promise<void>;
onBack?: () => void;
showBackButton?: boolean;
title?: string | null;
};
const THEME_OPTIONS = [
@@ -30,8 +34,11 @@ const DIFFICULTY_OPTIONS: Array<{ value: BarkBattleDifficultyPreset; label: stri
export function BarkBattleConfigEditor({
isBusy = false,
error: externalError = null,
onPublish,
onBack,
showBackButton = true,
title: headingTitle = '汪汪声浪大作战',
}: BarkBattleConfigEditorProps) {
const [title, setTitle] = useState('我的声浪竞技场');
const [description, setDescription] = useState('');
@@ -40,7 +47,7 @@ export function BarkBattleConfigEditor({
const [opponentDogSkinPreset, setOpponentDogSkinPreset] = useState('husky');
const [difficultyPreset, setDifficultyPreset] = useState<BarkBattleDifficultyPreset>('normal');
const [leaderboardEnabled, setLeaderboardEnabled] = useState(true);
const [error, setError] = useState<string | null>(null);
const [localError, setLocalError] = useState<string | null>(null);
const payload = useMemo<BarkBattleConfigEditorPayload>(
() => ({
@@ -65,96 +72,212 @@ export function BarkBattleConfigEditor({
const handlePublish = () => {
if (!payload.title) {
setError('请先填写作品标题');
setLocalError('请先填写作品标题');
return;
}
setError(null);
setLocalError(null);
void onPublish(payload);
};
const visibleError = localError ?? externalError;
return (
<section className="min-h-screen bg-slate-950 px-4 py-6 text-slate-50 sm:px-6" aria-label="Bark Battle 轻配置编辑器">
<div className="mx-auto flex w-full max-w-5xl flex-col gap-5 lg:grid lg:grid-cols-[minmax(0,1fr)_360px]">
<div className="rounded-3xl border border-cyan-300/20 bg-slate-900/90 p-5 shadow-2xl shadow-cyan-950/40">
<div className="mb-5 flex items-start justify-between gap-3">
<div>
<p className="mb-2 inline-flex rounded-full bg-cyan-300/10 px-3 py-1 text-xs font-bold text-cyan-100"></p>
<h1 className="text-2xl font-black tracking-tight sm:text-3xl"></h1>
<p className="mt-2 text-sm text-slate-300"></p>
<section
className="platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col overflow-hidden"
aria-label="汪汪声浪轻配置编辑器"
>
{showBackButton && onBack ? (
<div className="mb-3 flex shrink-0 items-center justify-between gap-3 sm:mb-4">
<button
type="button"
onClick={onBack}
disabled={isBusy}
className={`platform-button platform-button--ghost min-h-0 self-start px-3 py-1.5 text-[11px] ${isBusy ? 'opacity-45' : ''}`}
>
<span className="inline-flex items-center gap-1.5">
<ArrowLeft className="h-3.5 w-3.5" />
</span>
</button>
</div>
) : null}
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
{headingTitle ? (
<div className="mb-3 shrink-0 sm:mb-5">
<div className="flex flex-wrap items-center gap-2">
<h1 className="m-0 text-3xl font-black leading-none tracking-normal text-[var(--platform-text-strong)] sm:text-7xl">
{headingTitle}
</h1>
<span className="rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-[11px] font-black text-emerald-700">
</span>
</div>
{onBack ? (
<button type="button" onClick={onBack} className="rounded-full border border-slate-600 px-3 py-2 text-sm text-slate-200">
</button>
</div>
) : null}
<div
className={`grid min-h-0 flex-1 grid-rows-[minmax(0,1fr)_auto] gap-3 lg:grid-cols-[minmax(0,1.12fr)_minmax(17rem,0.88fr)] lg:grid-rows-1 ${isBusy ? 'opacity-55' : ''}`}
>
<div className="flex min-h-0 flex-col gap-3 overflow-y-auto pr-0 lg:pr-1">
<label className="block shrink-0">
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
</span>
<input
value={title}
disabled={isBusy}
onChange={(event) => setTitle(event.target.value)}
className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-base font-semibold text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
maxLength={40}
aria-label="作品标题"
/>
</label>
<label className="block shrink-0">
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
</span>
<textarea
value={description}
disabled={isBusy}
onChange={(event) => setDescription(event.target.value)}
className="h-[5.5rem] min-h-[5.5rem] w-full resize-none rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base leading-6 text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
maxLength={160}
placeholder=""
aria-label="简介"
/>
</label>
<div className="grid shrink-0 gap-2.5 sm:grid-cols-2">
<label className="block">
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
</span>
<select
value={themePreset}
disabled={isBusy}
onChange={(event) => setThemePreset(event.target.value)}
className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-sm font-black text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
aria-label="主题背景"
>
{THEME_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<label className="block">
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
</span>
<select
value={difficultyPreset}
disabled={isBusy}
onChange={(event) =>
setDifficultyPreset(
event.target.value as BarkBattleDifficultyPreset,
)
}
className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-sm font-black text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
aria-label="难度预设"
>
{DIFFICULTY_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<label className="block">
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
</span>
<select
value={playerDogSkinPreset}
disabled={isBusy}
onChange={(event) =>
setPlayerDogSkinPreset(event.target.value)
}
className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-sm font-black text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
aria-label="玩家狗狗"
>
{DOG_SKIN_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<label className="block">
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
</span>
<select
value={opponentDogSkinPreset}
disabled={isBusy}
onChange={(event) =>
setOpponentDogSkinPreset(event.target.value)
}
className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-sm font-black text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
aria-label="对手狗狗"
>
{DOG_SKIN_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
</div>
<label className="flex shrink-0 items-center justify-between gap-3 rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-4 py-3 text-sm font-black text-[var(--platform-text-strong)] shadow-[inset_0_1px_0_rgba(255,255,255,0.76)]">
<span className="inline-flex items-center gap-2">
<Trophy className="h-4 w-4 text-amber-500" />
</span>
<input
aria-label="开启排行榜"
type="checkbox"
checked={leaderboardEnabled}
disabled={isBusy}
onChange={(event) =>
setLeaderboardEnabled(event.target.checked)
}
className="h-5 w-5 accent-[#ff4f6a]"
/>
</label>
{visibleError ? (
<div className="platform-banner platform-banner--danger shrink-0 rounded-2xl text-sm leading-6">
{visibleError}
</div>
) : null}
</div>
<div className="grid gap-4">
<label className="grid gap-2 text-sm font-semibold text-slate-200">
<input
value={title}
onChange={(event) => setTitle(event.target.value)}
className="rounded-2xl border border-slate-700 bg-slate-950 px-4 py-3 text-base text-white outline-none focus:border-cyan-300"
maxLength={40}
/>
</label>
<label className="grid gap-2 text-sm font-semibold text-slate-200">
<textarea
value={description}
onChange={(event) => setDescription(event.target.value)}
className="min-h-[88px] rounded-2xl border border-slate-700 bg-slate-950 px-4 py-3 text-base text-white outline-none focus:border-cyan-300"
maxLength={160}
placeholder="一句话告诉玩家这场声浪对决的氛围"
/>
</label>
<div className="grid gap-4 sm:grid-cols-2">
<label className="grid gap-2 text-sm font-semibold text-slate-200">
<select value={themePreset} onChange={(event) => setThemePreset(event.target.value)} className="rounded-2xl border border-slate-700 bg-slate-950 px-4 py-3 text-white">
{THEME_OPTIONS.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
</select>
</label>
<label className="grid gap-2 text-sm font-semibold text-slate-200">
<select value={difficultyPreset} onChange={(event) => setDifficultyPreset(event.target.value as BarkBattleDifficultyPreset)} className="rounded-2xl border border-slate-700 bg-slate-950 px-4 py-3 text-white">
{DIFFICULTY_OPTIONS.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
</select>
</label>
<label className="grid gap-2 text-sm font-semibold text-slate-200">
<select value={playerDogSkinPreset} onChange={(event) => setPlayerDogSkinPreset(event.target.value)} className="rounded-2xl border border-slate-700 bg-slate-950 px-4 py-3 text-white">
{DOG_SKIN_OPTIONS.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
</select>
</label>
<label className="grid gap-2 text-sm font-semibold text-slate-200">
<select value={opponentDogSkinPreset} onChange={(event) => setOpponentDogSkinPreset(event.target.value)} className="rounded-2xl border border-slate-700 bg-slate-950 px-4 py-3 text-white">
{DOG_SKIN_OPTIONS.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
</select>
</label>
</div>
<label className="flex items-center justify-between gap-3 rounded-2xl border border-slate-700 bg-slate-950 px-4 py-3 text-sm font-semibold text-slate-100">
<span>
<span className="block text-xs font-normal text-slate-400"></span>
</span>
<input aria-label="开启排行榜" type="checkbox" checked={leaderboardEnabled} onChange={(event) => setLeaderboardEnabled(event.target.checked)} className="h-5 w-5" />
</label>
{error ? <p className="rounded-2xl bg-rose-500/15 px-4 py-3 text-sm font-semibold text-rose-100">{error}</p> : null}
<button type="button" disabled={isBusy} onClick={handlePublish} className="rounded-full bg-cyan-200 px-5 py-3 text-sm font-black text-slate-950 disabled:opacity-50">
{isBusy ? '发布中…' : '发布并试玩'}
</button>
</div>
<BarkBattlePreviewCard config={payload} />
</div>
</div>
<BarkBattlePreviewCard config={payload} />
<div className="mt-2 flex shrink-0 justify-center pb-[max(0.25rem,env(safe-area-inset-bottom))] sm:mt-3">
<button
type="button"
disabled={isBusy}
onClick={handlePublish}
className={`platform-button platform-button--primary min-h-10 px-4 py-2 text-sm sm:min-h-11 sm:px-5 ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
>
<span className="inline-flex flex-wrap items-center justify-center gap-1.5 sm:gap-2">
{isBusy ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<WandSparkles className="h-4 w-4" />
)}
<span>{isBusy ? '发布中' : '发布并试玩'}</span>
</span>
</button>
</div>
</section>
);