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:
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user