Fix DashScope env loading for scene image generation

This commit is contained in:
2026-04-06 15:01:15 +08:00
parent fcd8d727b0
commit d678929064
23 changed files with 4943 additions and 138 deletions

View File

@@ -0,0 +1,376 @@
import { motion } from 'motion/react';
import type {
CustomWorldGenerationProgress,
} from '../services/ai';
import { AnimationState, type Character } from '../types';
import { getNineSliceStyle, UI_CHROME } from '../uiAssets';
import { CharacterAnimator } from './CharacterAnimator';
interface CustomWorldGenerationViewProps {
settingText: string;
actionPreviewCharacters: Character[];
progress: CustomWorldGenerationProgress | null;
isGenerating: boolean;
error: string | null;
onBack: () => void;
onEditSetting: () => void;
onRetry: () => void;
onInterrupt: () => void;
}
const ACTION_SHOWCASE: Array<{
label: string;
description: string;
state: AnimationState;
}> = [
{
label: '冲阵测试',
description: '检查角色前探、推进与开场压迫感。',
state: AnimationState.RUN,
},
{
label: '交战演示',
description: '预热战斗站姿与交锋节奏。',
state: AnimationState.ATTACK,
},
{
label: '驻场待命',
description: '确认角色在剧情停驻时的氛围姿态。',
state: AnimationState.IDLE,
},
] as const;
function formatDuration(ms: number) {
const safeMs = Math.max(0, Math.round(ms));
const totalSeconds = Math.ceil(safeMs / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
if (minutes <= 0) {
return `${Math.max(1, seconds)}`;
}
if (seconds === 0) {
return `${minutes} 分钟`;
}
return `${minutes}${seconds}`;
}
function getProgressPercentage(progress: CustomWorldGenerationProgress | null) {
return Math.max(0, Math.min(100, progress?.overallProgress ?? 0));
}
export function CustomWorldGenerationView({
settingText,
actionPreviewCharacters,
progress,
isGenerating,
error,
onBack,
onEditSetting,
onRetry,
onInterrupt,
}: CustomWorldGenerationViewProps) {
const progressValue = getProgressPercentage(progress);
const steps = progress?.steps ?? [];
const estimatedWaitText =
progress?.estimatedRemainingMs != null
? `预计还需 ${formatDuration(progress.estimatedRemainingMs)}`
: '正在校准预计等待时间';
const elapsedText =
progress != null ? `已耗时 ${formatDuration(progress.elapsedMs)}` : '正在启动世界生成';
return (
<div
className="flex h-full min-h-0 flex-col overflow-y-auto overscroll-y-contain pr-1 pb-[max(1rem,env(safe-area-inset-bottom))]"
style={{ WebkitOverflowScrolling: 'touch' }}
>
<div className="sticky top-0 z-20 -mx-3 mb-4 flex items-center justify-between gap-3 bg-[linear-gradient(180deg,rgba(10,12,18,0.96),rgba(10,12,18,0.86),rgba(10,12,18,0))] px-3 pb-3 pt-1 sm:static sm:mx-0 sm:bg-none sm:px-0 sm:pb-0 sm:pt-0">
<button
type="button"
onClick={onBack}
disabled={isGenerating}
className={`rounded-full border border-white/10 bg-black/18 px-3 py-1.5 text-[11px] text-zinc-300 transition-colors hover:text-white ${isGenerating ? 'cursor-not-allowed opacity-45' : ''}`}
>
</button>
<div className="rounded-full border border-sky-300/16 bg-sky-500/10 px-3 py-1 text-[10px] tracking-[0.2em] text-sky-100">
{isGenerating ? '世界建设中' : error ? '生成已暂停' : '等待操作'}
</div>
</div>
<div className="grid flex-none gap-4 xl:min-h-0 xl:flex-1 xl:grid-cols-[minmax(0,1.1fr)_minmax(22rem,0.9fr)]">
<div className="flex flex-col gap-4 xl:min-h-0">
<section
className="pixel-nine-slice pixel-panel overflow-hidden"
style={getNineSliceStyle(UI_CHROME.storyPanel, {
paddingX: 16,
paddingY: 14,
})}
>
<div className="mb-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0">
<div className="text-[11px] font-bold tracking-[0.2em] text-sky-100/85">
</div>
<div className="mt-1 text-sm text-zinc-400">
</div>
</div>
<button
type="button"
onClick={onEditSetting}
disabled={isGenerating}
className={`rounded-full border border-white/10 bg-black/20 px-3 py-1.5 text-[11px] text-zinc-300 transition-colors hover:text-white ${isGenerating ? 'cursor-not-allowed opacity-40' : ''}`}
>
</button>
</div>
<div className="rounded-2xl border border-white/8 bg-black/22 px-4 py-4 text-sm leading-7 text-zinc-200 md:max-h-[16rem] md:overflow-y-auto">
{settingText}
</div>
</section>
<section
className="pixel-nine-slice pixel-panel flex flex-col xl:min-h-0 xl:flex-1"
style={getNineSliceStyle(UI_CHROME.panel, {
paddingX: 16,
paddingY: 14,
})}
>
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
<div className="text-[11px] font-bold tracking-[0.2em] text-zinc-400">
</div>
<div className="mt-1 text-xl font-black leading-tight text-white sm:text-[2rem]">
{progress?.phaseLabel ?? '正在启动世界生成'}
</div>
<div className="mt-2 max-w-[36rem] text-sm leading-6 text-zinc-300">
{progress?.phaseDetail ?? '正在初始化世界生成链路与阶段监控。'}
</div>
</div>
<div className="shrink-0 sm:text-right">
<div className="text-[11px] tracking-[0.16em] text-zinc-500">
</div>
<div className="mt-1 text-3xl font-black text-sky-100 sm:text-4xl">
{progressValue}%
</div>
</div>
</div>
<div className="mt-4 h-4 overflow-hidden rounded-full border border-white/10 bg-black/35">
<motion.div
className="h-full bg-[linear-gradient(90deg,#38bdf8_0%,#67e8f9_45%,#fde68a_100%)]"
animate={{ width: `${progressValue}%` }}
transition={{ duration: 0.35, ease: 'easeOut' }}
/>
</div>
<div className="mt-4 grid gap-2 sm:grid-cols-3">
<div className="rounded-2xl border border-white/8 bg-black/20 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="rounded-2xl border border-white/8 bg-black/20 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">
{estimatedWaitText}
</div>
</div>
<div className="rounded-2xl border border-white/8 bg-black/20 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">
{elapsedText}
</div>
</div>
</div>
<div className="mt-4 space-y-2 xl:min-h-0 xl:flex-1 xl:overflow-y-auto xl:pr-1">
{steps.map((step) => (
<div
key={step.id}
className={`rounded-2xl border px-4 py-3 transition-colors ${
step.status === 'completed'
? 'border-emerald-400/16 bg-emerald-500/8'
: step.status === 'active'
? 'border-sky-300/22 bg-sky-500/10'
: 'border-white/8 bg-black/18'
}`}
>
<div className="flex items-center justify-between gap-3">
<div className="text-sm font-semibold text-white">
{step.label}
</div>
<div className="text-xs text-zinc-300">
{step.completed}/{step.total}
</div>
</div>
<div className="mt-1 text-xs leading-6 text-zinc-400">
{step.detail}
</div>
</div>
))}
</div>
{error ? (
<div className="mt-4 rounded-2xl border border-rose-400/18 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-100">
{error}
</div>
) : null}
<div className="mt-4 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:justify-end">
{!isGenerating ? (
<>
<button
type="button"
onClick={onEditSetting}
className="rounded-full border border-white/10 bg-black/20 px-4 py-2 text-sm text-zinc-300 transition-colors hover:text-white"
>
</button>
<button
type="button"
onClick={onRetry}
className="pixel-nine-slice pixel-pressable w-full text-left sm:w-auto"
style={getNineSliceStyle(UI_CHROME.choiceButton, {
paddingX: 16,
paddingY: 10,
})}
>
<div className="flex items-center justify-between gap-4">
<span className="text-sm font-semibold text-white">
</span>
<span className="text-white/60"></span>
</div>
</button>
</>
) : (
<button
type="button"
onClick={onInterrupt}
className="rounded-full border border-rose-300/18 bg-rose-500/10 px-4 py-2 text-sm text-rose-100 transition-colors hover:text-white"
>
</button>
)}
</div>
</section>
</div>
<div className="flex flex-col gap-4 xl:min-h-0">
<section
className="pixel-nine-slice pixel-panel relative overflow-hidden"
style={getNineSliceStyle(UI_CHROME.panel, {
paddingX: 16,
paddingY: 14,
})}
>
<motion.div
className="pointer-events-none absolute -left-8 top-0 h-36 w-36 rounded-full bg-sky-400/18 blur-3xl"
animate={{
opacity: [0.22, 0.48, 0.22],
scale: [0.92, 1.08, 0.92],
}}
transition={{ duration: 6.5, repeat: Infinity, ease: 'easeInOut' }}
/>
<motion.div
className="pointer-events-none absolute bottom-0 right-0 h-32 w-32 rounded-full bg-amber-200/12 blur-3xl"
animate={{ opacity: [0.18, 0.4, 0.18], scale: [1, 1.12, 1] }}
transition={{ duration: 7.2, repeat: Infinity, ease: 'easeInOut' }}
/>
<div className="relative z-10">
<div className="text-[11px] font-bold tracking-[0.2em] text-sky-100/85">
</div>
<div className="mt-2 text-xl font-black leading-tight text-white sm:text-2xl">
</div>
<div className="mt-3 max-w-[26rem] text-sm leading-6 text-zinc-300">
</div>
<div className="mt-5 grid grid-cols-2 gap-2 sm:grid-cols-3">
<div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3 text-center text-xs text-zinc-200">
</div>
<div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3 text-center text-xs text-zinc-200">
</div>
<div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3 text-center text-xs text-zinc-200 col-span-2 sm:col-span-1">
</div>
</div>
</div>
</section>
<section
className="pixel-nine-slice pixel-panel xl:min-h-0 xl:flex-1"
style={getNineSliceStyle(UI_CHROME.panel, {
paddingX: 16,
paddingY: 14,
})}
>
<div className="mb-3">
<div className="text-[11px] font-bold tracking-[0.2em] text-zinc-400">
</div>
<div className="mt-1 text-sm leading-6 text-zinc-300">
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-1 2xl:grid-cols-3">
{ACTION_SHOWCASE.map((showcase, index) => {
const character =
actionPreviewCharacters[
index % Math.max(1, actionPreviewCharacters.length)
];
return (
<div
key={showcase.label}
className="rounded-[1.5rem] border border-white/8 bg-black/22 px-4 py-4"
>
<div className="flex h-28 items-end justify-center overflow-hidden rounded-2xl border border-white/10 bg-[radial-gradient(circle_at_top,rgba(125,211,252,0.18),rgba(10,12,18,0.1)_38%,rgba(10,12,18,0.76)_100%)] sm:h-32">
{character ? (
<CharacterAnimator
state={showcase.state}
character={character}
className="h-full w-full"
imageClassName="object-bottom"
/>
) : null}
</div>
<div className="mt-3 text-sm font-semibold text-white">
{showcase.label}
</div>
<div className="mt-1 text-xs leading-6 text-zinc-400">
{showcase.description}
</div>
{character ? (
<div className="mt-3 rounded-full border border-sky-300/14 bg-sky-500/10 px-3 py-1 text-[10px] tracking-[0.16em] text-sky-100">
{character.name}
</div>
) : null}
</div>
);
})}
</div>
</section>
</div>
</div>
</div>
);
}