Refine creation tab UX, generation flow, and bindings
Large changes across frontend, backend and docs to align creation-tab and generation-page behavior with new product UI/UX and Spacetime bindings. Updated hermes decision-log and pitfalls with concrete rules (banner carousel, font sizing, unread-dot tokens, template-card layout, direct card->entry routing, separation of account balance vs prize pools, removal of global page card shell, generation progress milestones and unified circular progress, and background video handling). Added GenerationProgressHero component and media assets, plus generation-related UI/tests updates (CustomWorldGenerationView, BarkBattleGeneratingView, creation hub/cards, platform entry routing, index tests). Backend and contract updates include new category fields in admin API types and admin UI form/list, spacetime-client/module/migration changes and generated bindings script. Misc: many tests adjusted, new docs and plan files added, and several server-rs crate changes to support the updated creation/ generation workflows.
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import { motion } from 'motion/react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
import type { CustomWorldGenerationProgress } from '../../packages/shared/src/contracts/runtime';
|
||||
import type { CustomWorldStructuredAnchorEntry } from '../services/customWorldAgentGenerationProgress';
|
||||
import {
|
||||
GenerationCurrentStepCard,
|
||||
GenerationPageBackdrop,
|
||||
GenerationProgressHero,
|
||||
} from './GenerationProgressHero';
|
||||
|
||||
interface CustomWorldGenerationViewProps {
|
||||
settingText: string;
|
||||
@@ -81,6 +84,19 @@ function getStepStatusLabel(step: { status: string }) {
|
||||
return '待处理';
|
||||
}
|
||||
|
||||
function resolveCurrentGenerationStep(
|
||||
progress: CustomWorldGenerationProgress | null,
|
||||
) {
|
||||
const steps = progress?.steps ?? [];
|
||||
return (
|
||||
steps.find((step) => step.status === 'active') ??
|
||||
steps[progress?.activeStepIndex ?? -1] ??
|
||||
steps.find((step) => step.status === 'pending') ??
|
||||
steps.at(-1) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function buildFallbackRenderKey(
|
||||
value: string | null | undefined,
|
||||
fallback: string,
|
||||
@@ -89,49 +105,6 @@ 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 = [],
|
||||
@@ -155,42 +128,47 @@ export function CustomWorldGenerationView({
|
||||
structuredEmptyText = '正在整理当前设定结构,请稍后。',
|
||||
hideBatchModule = false,
|
||||
}: CustomWorldGenerationViewProps) {
|
||||
const isMobileGenerationLayout = useIsMobileGenerationLayout();
|
||||
void hideBatchModule;
|
||||
const progressValue = getProgressPercentage(progress);
|
||||
const steps = progress?.steps ?? [];
|
||||
const currentStep = resolveCurrentGenerationStep(progress);
|
||||
const currentStepProgress = currentStep
|
||||
? getStepProgressPercentage(currentStep)
|
||||
: progressValue;
|
||||
const currentStepLabel = currentStep?.label ?? progress?.phaseLabel ?? '准备生成';
|
||||
const currentStepStatusLabel = currentStep
|
||||
? getStepStatusLabel(currentStep)
|
||||
: isGenerating
|
||||
? '进行中'
|
||||
: '待处理';
|
||||
const hasStructuredAnchors = anchorEntries.length > 0;
|
||||
// 允许不同生成场景按需隐藏第二模块的说明和次级返回动作。
|
||||
const normalizedSettingActionLabel = settingActionLabel?.trim() ?? '';
|
||||
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)}`
|
||||
: '正在校准预计等待时间';
|
||||
? formatDuration(progress.estimatedRemainingMs)
|
||||
: '校准中';
|
||||
const elapsedText =
|
||||
progress != null
|
||||
? `已耗时 ${formatDuration(progress.elapsedMs)}`
|
||||
: '正在启动世界生成';
|
||||
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))]"
|
||||
className="relative isolate z-[1] -mx-3 -my-3 flex h-[calc(100%+1.5rem)] min-h-0 flex-col overflow-y-auto overscroll-y-contain bg-transparent px-4 pb-[max(1.25rem,env(safe-area-inset-bottom))] pt-4 text-[#3d1f10] sm:mx-0 sm:my-0 sm:h-full sm:rounded-[2rem] sm:px-5 sm:pt-5"
|
||||
style={{ WebkitOverflowScrolling: 'touch' }}
|
||||
>
|
||||
<div className="platform-sticky-fade sticky top-0 z-20 -mx-3 mb-4 flex items-center justify-between gap-3 px-3 pb-3 pt-1 backdrop-blur-sm sm:static sm:mx-0 sm:bg-none sm:px-0 sm:pb-0 sm:pt-0 sm:backdrop-blur-none">
|
||||
<GenerationPageBackdrop />
|
||||
<div className="relative z-10 mb-6 flex shrink-0 items-center justify-between gap-3 py-2 sm:mb-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
className="platform-button platform-button--ghost min-h-0 px-3 py-1.5 text-[11px]"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-transparent px-0 py-2 text-xs font-black text-[#171411] sm:text-sm"
|
||||
>
|
||||
{backLabel}
|
||||
<ArrowLeft className="h-5 w-5 shrink-0" strokeWidth={2.6} />
|
||||
<span className="break-keep">{backLabel}</span>
|
||||
</button>
|
||||
<div className="platform-pill platform-pill--cool px-3 py-1 text-[10px] tracking-[0.2em]">
|
||||
<div className="rounded-full border border-[#f05816] bg-white/72 px-3 py-1.5 text-[11px] font-black tracking-[0.08em] text-[#df6118] shadow-[0_12px_30px_rgba(214,77,31,0.08)] backdrop-blur-md sm:px-4 sm:text-xs">
|
||||
{isGenerating
|
||||
? activeBadgeLabel
|
||||
: error
|
||||
@@ -199,143 +177,26 @@ export function CustomWorldGenerationView({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-none flex-col gap-4 xl:grid xl:min-h-0 xl:flex-1 xl:grid-cols-[minmax(0,1.35fr)_minmax(22rem,0.65fr)] xl:items-stretch">
|
||||
<section className="platform-surface platform-surface--soft flex flex-col px-4 py-3.5 xl:min-h-0 xl:flex-1 xl:px-5 xl:py-4">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between xl:gap-6">
|
||||
<div className="min-w-0">
|
||||
<div className="text-[11px] font-bold tracking-[0.2em] text-zinc-400">
|
||||
{progressTitle}
|
||||
</div>
|
||||
<div className="mt-1 text-xl font-black leading-tight text-[var(--platform-text-strong)] sm:text-[2rem] xl:text-[2.4rem]">
|
||||
{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-[var(--platform-cool-text)] sm:text-4xl">
|
||||
{progressValue}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative z-10 flex flex-none flex-col gap-4">
|
||||
<section className="overflow-hidden px-0 pb-2 pt-0 sm:px-0">
|
||||
<GenerationProgressHero
|
||||
title={progressTitle}
|
||||
phaseLabel={progress?.phaseLabel ?? '正在启动世界生成'}
|
||||
progressValue={progressValue}
|
||||
estimatedWaitText={estimatedWaitText}
|
||||
elapsedText={elapsedText}
|
||||
/>
|
||||
|
||||
<div className="platform-progress-track mt-4 h-4 overflow-hidden rounded-full xl:mt-5 xl:h-5">
|
||||
<motion.div
|
||||
className="h-full bg-[linear-gradient(90deg,#df7f40_0%,#cc754c_52%,#eaccb3_100%)]"
|
||||
animate={{ width: `${progressValue}%` }}
|
||||
transition={{ duration: 0.35, ease: 'easeOut' }}
|
||||
<div className="mt-[-0.15rem] px-0 sm:px-0">
|
||||
<GenerationCurrentStepCard
|
||||
label={currentStepLabel}
|
||||
statusLabel={currentStepStatusLabel}
|
||||
progressValue={currentStepProgress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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="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 break-keep text-xs font-semibold text-white sm:text-sm">
|
||||
{estimatedWaitText}
|
||||
</div>
|
||||
</div>
|
||||
<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 break-keep text-xs font-semibold text-white sm:text-sm">
|
||||
{elapsedText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-2 xl:grid xl:min-h-0 xl:flex-1 xl:grid-cols-2 xl:content-start xl:gap-2 xl:space-y-0 xl:overflow-y-auto xl:pr-1">
|
||||
{steps.map((step, index) => {
|
||||
const stepProgress = getStepProgressPercentage(step);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={buildFallbackRenderKey(
|
||||
step.id,
|
||||
`progress-step-${index}`,
|
||||
)}
|
||||
className={`rounded-2xl border px-4 py-3 transition-colors ${
|
||||
step.status === 'completed'
|
||||
? 'border-[var(--platform-success-border)] bg-[var(--platform-success-bg)]'
|
||||
: step.status === 'active'
|
||||
? 'border-[var(--platform-cool-border)] bg-[var(--platform-cool-bg)]'
|
||||
: '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">
|
||||
{step.label}
|
||||
</div>
|
||||
<div className="shrink-0 text-xs font-semibold text-zinc-300">
|
||||
{getStepStatusLabel(step)} {stepProgress}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 h-2 overflow-hidden rounded-full bg-white/8">
|
||||
<motion.div
|
||||
className={`h-full rounded-full ${
|
||||
step.status === 'completed'
|
||||
? 'bg-[var(--platform-success-text)]'
|
||||
: step.status === 'active'
|
||||
? 'bg-[linear-gradient(90deg,#df7f40_0%,#cc754c_56%,#eaccb3_100%)]'
|
||||
: 'bg-white/18'
|
||||
}`}
|
||||
animate={{ width: `${stepProgress}%` }}
|
||||
transition={{ duration: 0.45, ease: 'easeOut' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 text-xs leading-6 text-zinc-400">
|
||||
{step.detail}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="mt-4 rounded-2xl border border-[var(--platform-button-danger-border)] bg-[var(--platform-button-danger-fill)] px-4 py-3 text-sm leading-6 text-[var(--platform-button-danger-text)]">
|
||||
<div className="mt-4 rounded-[1.4rem] border border-[#d88969]/35 bg-white/76 px-4 py-3 text-sm leading-6 text-[#a6402f] backdrop-blur-md">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -372,14 +233,14 @@ export function CustomWorldGenerationView({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="platform-surface platform-surface--soft overflow-hidden px-4 py-3.5 xl:flex xl:min-h-0 xl:flex-col xl:px-5 xl:py-4">
|
||||
<div className="mb-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between xl:flex-col xl:items-start xl:gap-2">
|
||||
<section className="overflow-hidden rounded-[1.75rem] border border-[#eadcd1] bg-[rgba(255,250,246,0.92)] px-4 py-4 shadow-[0_20px_56px_rgba(112,57,30,0.08)] backdrop-blur-[5px] sm:px-5 sm:py-5">
|
||||
<div className="mb-4 flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-[11px] font-bold tracking-[0.2em] text-[var(--platform-cool-text)]">
|
||||
<div className="text-[13px] font-black tracking-[0.08em] text-[#111111]">
|
||||
{settingTitle}
|
||||
</div>
|
||||
{hasSettingDescription ? (
|
||||
<div className="mt-1 text-sm text-zinc-400">
|
||||
<div className="mt-2 text-[13px] leading-6 text-[#7e6656]">
|
||||
{normalizedSettingDescription}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -396,26 +257,26 @@ export function CustomWorldGenerationView({
|
||||
) : null}
|
||||
</div>
|
||||
{hasStructuredAnchors ? (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:min-h-0 xl:flex-1 xl:grid-cols-1 xl:overflow-y-auto xl:pr-1">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{anchorEntries.map((entry, index) => (
|
||||
<div
|
||||
key={buildFallbackRenderKey(
|
||||
entry.id,
|
||||
`anchor-entry-${index}`,
|
||||
)}
|
||||
className="platform-subpanel rounded-2xl px-4 py-4 xl:py-3"
|
||||
className="rounded-[1.15rem] border border-[#ead6c7] bg-white/74 px-4 py-4"
|
||||
>
|
||||
<div className="text-[11px] font-bold tracking-[0.18em] text-zinc-500">
|
||||
<div className="text-[9px] font-bold tracking-[0.12em] text-[#8e6f5d] sm:text-[10px]">
|
||||
{entry.label}
|
||||
</div>
|
||||
<div className="mt-2 whitespace-pre-line text-sm leading-7 text-zinc-100">
|
||||
<div className="mt-2 whitespace-pre-line text-[13px] leading-7 text-[#111111]">
|
||||
{entry.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="platform-subpanel whitespace-pre-line rounded-2xl px-4 py-4 text-sm leading-7 text-zinc-200 md:max-h-[16rem] md:overflow-y-auto xl:max-h-none xl:min-h-0 xl:flex-1">
|
||||
<div className="whitespace-pre-line rounded-[1.15rem] border border-[#ead6c7] bg-white/74 px-4 py-4 text-[13px] leading-7 text-[#111111] md:max-h-[16rem] md:overflow-y-auto">
|
||||
{settingText || structuredEmptyText}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user