Merge remote-tracking branch 'origin/master' into codex/editor-asset-library

# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
This commit is contained in:
2026-06-17 20:52:41 +08:00
68 changed files with 2279 additions and 2676 deletions
@@ -5,7 +5,10 @@ import {
resolveMiniGameGenerationProgressTickState,
resolveMiniGameGenerationViewBusy,
} from './PlatformEntryFlowShellImpl';
import { buildExternalGenerationQueueStatus } from './platformExternalGenerationQueueStatusModel';
import {
buildExternalGenerationQueuePresentation,
buildExternalGenerationQueueStatus,
} from './platformExternalGenerationQueueStatusModel';
import { resolveFinishedMiniGameDraftGenerationState } from './platformMiniGameDraftGenerationStateModel';
describe('resolveMiniGameGenerationProgressTickState', () => {
@@ -88,4 +91,36 @@ describe('buildExternalGenerationQueueStatus', () => {
test('没有队列或任务信息时不显示状态条', () => {
expect(buildExternalGenerationQueueStatus(null, null)).toBeNull();
});
test('构造我的页生成队列展示状态', () => {
expect(
buildExternalGenerationQueuePresentation({
currentStatus: 'running',
currentProgress: 42.4,
pendingCount: 2,
runningCount: 1,
}),
).toEqual({
statusLabel: '生成中',
progressLabel: '42%',
pendingLabel: '2',
runningLabel: '1',
pendingCount: 2,
runningCount: 1,
progress: 42,
shouldShow: true,
});
expect(buildExternalGenerationQueuePresentation(null).shouldShow).toBe(
false,
);
expect(
buildExternalGenerationQueuePresentation({
currentStatus: 'completed',
currentProgress: 100,
pendingCount: 0,
runningCount: 0,
}).shouldShow,
).toBe(false);
});
});
@@ -4798,13 +4798,18 @@ export function PlatformEntryFlowShellImpl({
woodenFishGenerationState,
);
const shouldShowExternalGenerationQueueStatus =
isExternalGenerationQueueStage(selectionStage);
isExternalGenerationQueueStage(selectionStage) ||
(selectionStage === 'platform' &&
platformBootstrap.platformTab === 'profile' &&
platformBootstrap.canReadProtectedData);
useEffect(() => {
if (!shouldShowExternalGenerationQueueStatus) {
setExternalGenerationQueueOverview(null);
return;
}
setExternalGenerationQueueOverview(null);
let disposed = false;
let controller: AbortController | null = null;
@@ -4832,47 +4837,44 @@ export function PlatformEntryFlowShellImpl({
controller?.abort();
window.clearInterval(intervalId);
};
}, [shouldShowExternalGenerationQueueStatus]);
const puzzleExternalGenerationQueueStatus = useMemo(
() =>
buildExternalGenerationQueueStatus(
externalGenerationQueueOverview,
puzzleOperation?.queueState ?? null,
),
[externalGenerationQueueOverview, puzzleOperation],
);
const jumpHopExternalGenerationQueueStatus = useMemo(
() =>
buildExternalGenerationQueueStatus(
externalGenerationQueueOverview,
jumpHopQueueState,
),
[externalGenerationQueueOverview, jumpHopQueueState],
);
const puzzleClearExternalGenerationQueueStatus = useMemo(
() =>
buildExternalGenerationQueueStatus(
externalGenerationQueueOverview,
puzzleClearQueueState,
),
[externalGenerationQueueOverview, puzzleClearQueueState],
);
const woodenFishExternalGenerationQueueStatus = useMemo(
() =>
buildExternalGenerationQueueStatus(
externalGenerationQueueOverview,
woodenFishQueueState,
),
[externalGenerationQueueOverview, woodenFishQueueState],
);
}, [authUi?.user?.id, shouldShowExternalGenerationQueueStatus]);
const activeExternalGenerationJobState = useMemo(() => {
const candidates = [
puzzleOperation?.queueState ?? null,
jumpHopQueueState,
puzzleClearQueueState,
woodenFishQueueState,
].filter((candidate): candidate is ExternalGenerationJobStatusRecord =>
Boolean(candidate),
);
return (
candidates.find(
(candidate) =>
candidate.status === 'queued' || candidate.status === 'running',
) ??
candidates[0] ??
null
);
}, [
jumpHopQueueState,
puzzleClearQueueState,
puzzleOperation?.queueState,
woodenFishQueueState,
]);
const externalGenerationQueueStatus = useMemo(
() =>
buildExternalGenerationQueueStatus(
externalGenerationQueueOverview,
null,
activeExternalGenerationJobState,
),
[externalGenerationQueueOverview],
[activeExternalGenerationJobState, externalGenerationQueueOverview],
);
const profileExternalGenerationQueueStatus =
platformBootstrap.canReadProtectedData &&
platformBootstrap.platformTab === 'profile'
? externalGenerationQueueStatus
: null;
const platformBootstrapErrorForDisplay = isCreationEntryDisabledErrorMessage(
platformBootstrap.platformError,
)
@@ -15213,6 +15215,7 @@ export function PlatformEntryFlowShellImpl({
isLoadingDashboard={platformBootstrap.isLoadingDashboard}
hasUnreadDraftUpdate={hasUnreadDraftUpdates}
profileTaskRefreshKey={profileTaskRefreshKey}
profileGenerationQueueStatus={profileExternalGenerationQueueStatus}
isDesktopLayout={isDesktopLayout}
isResumingSaveWorldKey={platformBootstrap.isResumingSaveWorldKey}
platformError={
@@ -15649,7 +15652,6 @@ export function PlatformEntryFlowShellImpl({
activeBadgeLabel="草稿生成中"
pausedBadgeLabel="草稿生成已暂停"
idleBadgeLabel="等待返回工作区"
queueStatus={jumpHopExternalGenerationQueueStatus}
/>
</Suspense>
</motion.div>
@@ -15794,7 +15796,6 @@ export function PlatformEntryFlowShellImpl({
setSelectionStage('match3d-agent-workspace');
}}
onRetry={retryMatch3DDraftGeneration}
queueStatus={puzzleClearExternalGenerationQueueStatus}
hideBatchModule
/>
</Suspense>
@@ -16065,7 +16066,6 @@ export function PlatformEntryFlowShellImpl({
activeBadgeLabel="草稿生成中"
pausedBadgeLabel="草稿生成已暂停"
idleBadgeLabel="等待返回工作区"
queueStatus={woodenFishExternalGenerationQueueStatus}
/>
</Suspense>
</motion.div>
@@ -16266,7 +16266,6 @@ export function PlatformEntryFlowShellImpl({
activeBadgeLabel="图片生成中"
pausedBadgeLabel="图片生成已暂停"
idleBadgeLabel="等待返回结果页"
queueStatus={externalGenerationQueueStatus}
/>
</Suspense>
</motion.div>
@@ -16471,7 +16470,6 @@ export function PlatformEntryFlowShellImpl({
setSelectionStage('jump-hop-workspace');
}}
onRetry={retryJumpHopDraftGeneration}
queueStatus={externalGenerationQueueStatus}
/>
</Suspense>
</motion.div>
@@ -16620,7 +16618,6 @@ export function PlatformEntryFlowShellImpl({
activeBadgeLabel="素材生成中"
pausedBadgeLabel="素材生成已暂停"
idleBadgeLabel="等待返回工作区"
queueStatus={externalGenerationQueueStatus}
/>
</Suspense>
</motion.div>
@@ -16750,7 +16747,6 @@ export function PlatformEntryFlowShellImpl({
setSelectionStage('wooden-fish-workspace');
}}
onRetry={retryWoodenFishDraftGeneration}
queueStatus={externalGenerationQueueStatus}
/>
</Suspense>
</motion.div>
@@ -16944,7 +16940,6 @@ export function PlatformEntryFlowShellImpl({
setSelectionStage('puzzle-agent-workspace');
}}
onRetry={retryPuzzleDraftGeneration}
queueStatus={puzzleExternalGenerationQueueStatus}
hideBatchModule
/>
</Suspense>
@@ -17071,7 +17066,6 @@ export function PlatformEntryFlowShellImpl({
activeBadgeLabel="草稿生成中"
pausedBadgeLabel="草稿生成已暂停"
idleBadgeLabel="等待返回工作区"
queueStatus={externalGenerationQueueStatus}
/>
</Suspense>
</motion.div>
@@ -17315,7 +17309,6 @@ export function PlatformEntryFlowShellImpl({
activeBadgeLabel="草稿编译中"
pausedBadgeLabel="草稿生成已暂停"
idleBadgeLabel="等待返回工作区"
queueStatus={externalGenerationQueueStatus}
/>
</Suspense>
</motion.div>
@@ -0,0 +1,84 @@
import { Loader2 } from 'lucide-react';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { PlatformProgressBar } from '../common/PlatformProgressBar';
import {
buildExternalGenerationQueuePresentation,
type ExternalGenerationQueueStatus,
} from './platformExternalGenerationQueueStatusModel';
type PlatformProfileGenerationQueueCardProps = {
queueStatus: ExternalGenerationQueueStatus | null;
};
/**
* “我的”页里的后台生成状态卡。
* 只展示当前账号可见的队列概览,业务完成仍以各玩法草稿 / 作品回读为准。
*/
export function PlatformProfileGenerationQueueCard({
queueStatus,
}: PlatformProfileGenerationQueueCardProps) {
const presentation = buildExternalGenerationQueuePresentation(queueStatus);
if (!presentation.shouldShow) {
return null;
}
const progressValue = presentation.progress ?? 0;
return (
<section
className="platform-profile-generation-queue-card"
aria-label="生成队列"
>
<div className="flex min-w-0 items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="platform-profile-generation-queue-card__icon">
<Loader2 className="h-4 w-4" />
</span>
<span className="text-[15px] font-black text-[var(--platform-text-strong)]">
</span>
</div>
<div className="mt-1 text-[12px] font-medium text-[var(--platform-text-base)]">
{presentation.statusLabel ?? '等待生成任务'}
</div>
</div>
{presentation.statusLabel ? (
<PlatformPillBadge
tone={
queueStatus?.currentStatus === 'failed' ? 'danger' : 'profile'
}
size="xs"
className="shrink-0 px-2.5 py-1"
>
{presentation.progressLabel
? `${presentation.statusLabel} ${presentation.progressLabel}`
: presentation.statusLabel}
</PlatformPillBadge>
) : null}
</div>
<PlatformProgressBar
value={progressValue}
indeterminate={presentation.progress == null}
ariaLabel="生成队列进度"
size="sm"
className="mt-3 bg-[rgba(240,226,214,0.88)]"
fillClassName="bg-[linear-gradient(90deg,#e47631,#ce5f2a)]"
/>
<div className="mt-3 grid grid-cols-2 gap-2">
<div className="platform-profile-generation-queue-card__metric">
<span></span>
<strong>{presentation.pendingLabel}</strong>
</div>
<div className="platform-profile-generation-queue-card__metric">
<span></span>
<strong>{presentation.runningLabel}</strong>
</div>
</div>
</section>
);
}
@@ -2,7 +2,24 @@ import type {
ExternalGenerationJobStatusRecord,
ExternalGenerationQueueOverview,
} from '../../../packages/shared/src/contracts/externalGeneration';
import type { ExternalGenerationQueueStatus } from '../CustomWorldGenerationView';
export type ExternalGenerationQueueStatus = {
currentStatus?: 'queued' | 'running' | 'completed' | 'failed' | null;
currentProgress?: number | null;
pendingCount?: number | null;
runningCount?: number | null;
};
export type ExternalGenerationQueuePresentation = {
statusLabel: string | null;
progressLabel: string | null;
pendingLabel: string;
runningLabel: string;
pendingCount: number;
runningCount: number;
progress: number | null;
shouldShow: boolean;
};
export function buildExternalGenerationQueueStatus(
overview: ExternalGenerationQueueOverview | null,
@@ -19,3 +36,97 @@ export function buildExternalGenerationQueueStatus(
runningCount: overview?.runningCount ?? null,
};
}
export function resolveExternalGenerationQueueStatusLabel(
status: ExternalGenerationQueueStatus['currentStatus'],
) {
if (status === 'queued') {
return '排队中';
}
if (status === 'running') {
return '生成中';
}
if (status === 'failed') {
return '生成失败';
}
if (status === 'completed') {
return '已完成';
}
return null;
}
function resolveExternalGenerationQueueOverviewLabel(
pendingCount: number,
runningCount: number,
) {
if (runningCount > 0) {
return '生成中';
}
if (pendingCount > 0) {
return '排队中';
}
return null;
}
export function normalizeExternalGenerationQueueCount(
value: number | null | undefined,
) {
if (typeof value !== 'number' || !Number.isFinite(value)) {
return 0;
}
return Math.max(0, Math.round(value));
}
export function normalizeExternalGenerationQueueProgress(
value: number | null | undefined,
) {
if (typeof value !== 'number' || !Number.isFinite(value)) {
return null;
}
return Math.max(0, Math.min(100, Math.round(value)));
}
export function buildExternalGenerationQueuePresentation(
status: ExternalGenerationQueueStatus | null | undefined,
): ExternalGenerationQueuePresentation {
const pendingCount = normalizeExternalGenerationQueueCount(
status?.pendingCount,
);
const runningCount = normalizeExternalGenerationQueueCount(
status?.runningCount,
);
const progress = normalizeExternalGenerationQueueProgress(
status?.currentProgress,
);
const isCompletedCurrentJob = status?.currentStatus === 'completed';
const currentStatusLabel = resolveExternalGenerationQueueStatusLabel(
status?.currentStatus ?? null,
);
const overviewStatusLabel = resolveExternalGenerationQueueOverviewLabel(
pendingCount,
runningCount,
);
const statusLabel = isCompletedCurrentJob
? overviewStatusLabel
: (currentStatusLabel ?? overviewStatusLabel);
const progressValue = isCompletedCurrentJob ? null : progress;
return {
statusLabel,
progressLabel: progressValue == null ? null : `${progressValue}%`,
pendingLabel: pendingCount.toString(),
runningLabel: runningCount.toString(),
pendingCount,
runningCount,
progress: progressValue,
shouldShow: Boolean(statusLabel || pendingCount > 0 || runningCount > 0),
};
}