Merge remote-tracking branch 'origin/master' into dev-jenken
# Conflicts: # .hermes/shared-memory/pitfalls.md # server-rs/crates/api-server/src/modules/jump_hop.rs # src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx # src/services/jump-hop/jumpHopClient.test.ts
This commit is contained in:
@@ -525,6 +525,7 @@ test('creation start card maps backend jump-hop draft to template card', () => {
|
||||
profileId: 'jump-hop-profile-1',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'jump-hop-session-1',
|
||||
themeText: '跳一跳生成草稿',
|
||||
workTitle: '跳一跳生成草稿',
|
||||
workDescription: '后端仍在生成跳一跳玩法。',
|
||||
themeTags: ['跳一跳'],
|
||||
|
||||
@@ -1,144 +1,205 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { JumpHopDraftResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import type { JumpHopWorkProfileResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import { useJumpHopLeaderboard } from '../../services/jump-hop/useJumpHopLeaderboard';
|
||||
import { JumpHopResultView } from './JumpHopResultView';
|
||||
|
||||
const draft: JumpHopDraftResponse = {
|
||||
templateId: 'jump-hop',
|
||||
templateName: '跳一跳',
|
||||
profileId: 'profile-1',
|
||||
workTitle: '云端跳台',
|
||||
workDescription: '一路跳到星星。',
|
||||
themeTags: ['云朵', '星空'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'paper-toy',
|
||||
characterPrompt: '纸片小兔',
|
||||
tilePrompt: '柔软云朵平台',
|
||||
endMoodPrompt: '星光门',
|
||||
characterAsset: {
|
||||
assetId: 'character-1',
|
||||
imageSrc: 'data:image/png;base64,character',
|
||||
imageObjectKey: 'jump-hop/character.png',
|
||||
assetObjectId: 'asset-character',
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: '角色图',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
tileAtlasAsset: {
|
||||
assetId: 'tiles-1',
|
||||
imageSrc: 'data:image/png;base64,tiles',
|
||||
imageObjectKey: 'jump-hop/tiles.png',
|
||||
assetObjectId: 'asset-tiles',
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: '地块图',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
tileAssets: [
|
||||
{
|
||||
tileType: 'start',
|
||||
imageSrc: 'data:image/png;base64,tile-start',
|
||||
imageObjectKey: 'jump-hop/tile-start.png',
|
||||
assetObjectId: 'asset-tile-start',
|
||||
sourceAtlasCell: 'A1',
|
||||
visualWidth: 128,
|
||||
visualHeight: 96,
|
||||
topSurfaceRadius: 24,
|
||||
landingRadius: 28,
|
||||
},
|
||||
{
|
||||
tileType: 'finish',
|
||||
imageSrc: 'data:image/png;base64,tile-finish',
|
||||
imageObjectKey: 'jump-hop/tile-finish.png',
|
||||
assetObjectId: 'asset-tile-finish',
|
||||
sourceAtlasCell: 'A2',
|
||||
visualWidth: 128,
|
||||
visualHeight: 96,
|
||||
topSurfaceRadius: 24,
|
||||
landingRadius: 28,
|
||||
},
|
||||
],
|
||||
path: {
|
||||
seed: 'jump-hop-seed',
|
||||
difficulty: 'standard',
|
||||
platforms: [
|
||||
{
|
||||
platformId: 'platform-1',
|
||||
tileType: 'start',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 48,
|
||||
height: 36,
|
||||
landingRadius: 22,
|
||||
perfectRadius: 12,
|
||||
scoreValue: 1,
|
||||
},
|
||||
{
|
||||
platformId: 'platform-2',
|
||||
tileType: 'finish',
|
||||
x: 16,
|
||||
y: 18,
|
||||
width: 60,
|
||||
height: 42,
|
||||
landingRadius: 22,
|
||||
perfectRadius: 12,
|
||||
scoreValue: 2,
|
||||
},
|
||||
],
|
||||
finishIndex: 1,
|
||||
cameraPreset: 'default',
|
||||
scoring: {
|
||||
chargeToDistanceRatio: 1.2,
|
||||
maxChargeMs: 1800,
|
||||
hitBonus: 20,
|
||||
perfectBonus: 50,
|
||||
},
|
||||
},
|
||||
coverComposite: 'data:image/png;base64,cover',
|
||||
generationStatus: 'ready',
|
||||
};
|
||||
vi.mock('../../services/jump-hop/useJumpHopLeaderboard', () => ({
|
||||
useJumpHopLeaderboard: vi.fn(),
|
||||
}));
|
||||
|
||||
test('jump hop result view exposes test run and publish actions', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onBack = vi.fn();
|
||||
const onEdit = vi.fn();
|
||||
const onStartTestRun = vi.fn();
|
||||
const onPublish = vi.fn();
|
||||
const onRegenerateCharacter = vi.fn();
|
||||
const onRegenerateTiles = vi.fn();
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(useJumpHopLeaderboard).mockReturnValue({
|
||||
leaderboard: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
test('跳一跳结果页展示排行榜列表', () => {
|
||||
vi.mocked(useJumpHopLeaderboard).mockReturnValue({
|
||||
leaderboard: {
|
||||
profileId: 'jump-hop-profile-test',
|
||||
items: [
|
||||
{
|
||||
rank: 1,
|
||||
playerId: 'player-1',
|
||||
successfulJumpCount: 12,
|
||||
durationMs: 40123,
|
||||
updatedAt: '2026-05-27T00:00:00Z',
|
||||
},
|
||||
{
|
||||
rank: 2,
|
||||
playerId: 'player-2',
|
||||
successfulJumpCount: 10,
|
||||
durationMs: 38210,
|
||||
updatedAt: '2026-05-26T00:00:00Z',
|
||||
},
|
||||
],
|
||||
viewerBest: null,
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
|
||||
render(
|
||||
<JumpHopResultView
|
||||
profile={draft}
|
||||
onBack={onBack}
|
||||
onEdit={onEdit}
|
||||
onStartTestRun={onStartTestRun}
|
||||
onPublish={onPublish}
|
||||
onRegenerateCharacter={onRegenerateCharacter}
|
||||
onRegenerateTiles={onRegenerateTiles}
|
||||
profile={buildProfile({ publicationStatus: 'published' })}
|
||||
onBack={() => {}}
|
||||
onEdit={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
onPublish={() => {}}
|
||||
onRegenerateTiles={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('云端跳台')).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '试玩' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '发布' })).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '试玩' }));
|
||||
await user.click(screen.getByRole('button', { name: '发布' }));
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
await user.click(screen.getByRole('button', { name: '返回编辑' }));
|
||||
await user.click(screen.getByRole('button', { name: '角色' }));
|
||||
await user.click(screen.getByRole('button', { name: '地块' }));
|
||||
|
||||
expect(onStartTestRun).toHaveBeenCalledTimes(1);
|
||||
expect(onPublish).toHaveBeenCalledTimes(1);
|
||||
expect(onBack).toHaveBeenCalledTimes(1);
|
||||
expect(onEdit).toHaveBeenCalledTimes(1);
|
||||
expect(onRegenerateCharacter).toHaveBeenCalledTimes(1);
|
||||
expect(onRegenerateTiles).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByText('排行榜')).toBeTruthy();
|
||||
expect(screen.getByText('player-1')).toBeTruthy();
|
||||
expect(screen.getByText('12 跳')).toBeTruthy();
|
||||
expect(screen.getByText('00:40')).toBeTruthy();
|
||||
expect(screen.getByText('player-2')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('跳一跳结果页默认角色预览使用陶泥儿透明 logo', () => {
|
||||
render(
|
||||
<JumpHopResultView
|
||||
profile={buildProfile()}
|
||||
onBack={() => {}}
|
||||
onEdit={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
onPublish={() => {}}
|
||||
onRegenerateTiles={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('jump-hop-result-character-logo').getAttribute('src')).toBe(
|
||||
'/branding/jump-hop-taonier-character.png',
|
||||
);
|
||||
});
|
||||
|
||||
test('跳一跳草稿结果页不请求公开排行榜', () => {
|
||||
render(
|
||||
<JumpHopResultView
|
||||
profile={buildProfile({ publicationStatus: 'draft' })}
|
||||
onBack={() => {}}
|
||||
onEdit={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
onPublish={() => {}}
|
||||
onRegenerateTiles={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(useJumpHopLeaderboard).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText('排行榜')).toBeNull();
|
||||
});
|
||||
|
||||
function buildProfile(
|
||||
options: {
|
||||
publicationStatus?: JumpHopWorkProfileResponse['summary']['publicationStatus'];
|
||||
} = {},
|
||||
): JumpHopWorkProfileResponse {
|
||||
return {
|
||||
summary: {
|
||||
runtimeKind: 'jump-hop',
|
||||
workId: 'jump-hop-profile-test',
|
||||
profileId: 'jump-hop-profile-test',
|
||||
ownerUserId: 'user-test',
|
||||
sourceSessionId: 'jump-hop-session-test',
|
||||
themeText: '测试',
|
||||
workTitle: '测试',
|
||||
workDescription: '测试',
|
||||
themeTags: ['测试'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'minimal-blocks',
|
||||
coverImageSrc: null,
|
||||
publicationStatus: options.publicationStatus ?? 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-27T00:00:00Z',
|
||||
publishedAt: null,
|
||||
publishReady: true,
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
draft: {
|
||||
templateId: 'jump-hop',
|
||||
templateName: '跳一跳',
|
||||
profileId: 'jump-hop-profile-test',
|
||||
themeText: '测试',
|
||||
workTitle: '测试',
|
||||
workDescription: '测试',
|
||||
themeTags: ['测试'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'minimal-blocks',
|
||||
defaultCharacter: {
|
||||
characterId: 'jump-hop-default-runner',
|
||||
displayName: '默认角色',
|
||||
modelKind: 'builtin-three',
|
||||
bodyColor: '#f59e0b',
|
||||
accentColor: '#2563eb',
|
||||
},
|
||||
characterPrompt: '默认角色',
|
||||
tilePrompt: '地块',
|
||||
endMoodPrompt: null,
|
||||
characterAsset: {
|
||||
assetId: 'builtin',
|
||||
imageSrc: 'builtin://jump-hop/default-character',
|
||||
imageObjectKey: '',
|
||||
assetObjectId: 'builtin',
|
||||
generationProvider: 'builtin-three',
|
||||
prompt: '默认角色',
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
tileAtlasAsset: {
|
||||
assetId: 'builtin',
|
||||
imageSrc: 'builtin://jump-hop/default-character',
|
||||
imageObjectKey: '',
|
||||
assetObjectId: 'builtin',
|
||||
generationProvider: 'builtin-three',
|
||||
prompt: '默认角色',
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
tileAssets: [],
|
||||
path: null,
|
||||
coverComposite: null,
|
||||
backButtonAsset: null,
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
path: null as never,
|
||||
defaultCharacter: {
|
||||
characterId: 'jump-hop-default-runner',
|
||||
displayName: '默认角色',
|
||||
modelKind: 'builtin-three',
|
||||
bodyColor: '#f59e0b',
|
||||
accentColor: '#2563eb',
|
||||
},
|
||||
characterAsset: {
|
||||
assetId: 'builtin',
|
||||
imageSrc: 'builtin://jump-hop/default-character',
|
||||
imageObjectKey: '',
|
||||
assetObjectId: 'builtin',
|
||||
generationProvider: 'builtin-three',
|
||||
prompt: '默认角色',
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
tileAtlasAsset: {
|
||||
assetId: 'builtin',
|
||||
imageSrc: 'builtin://jump-hop/default-character',
|
||||
imageObjectKey: '',
|
||||
assetObjectId: 'builtin',
|
||||
generationProvider: 'builtin-three',
|
||||
prompt: '默认角色',
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
tileAssets: [],
|
||||
backButtonAsset: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,18 +2,22 @@ import {
|
||||
ArrowLeft,
|
||||
Loader2,
|
||||
Play,
|
||||
RefreshCcw,
|
||||
Send,
|
||||
Shuffle,
|
||||
} from 'lucide-react';
|
||||
import { type CSSProperties, useMemo, useState } from 'react';
|
||||
import { type CSSProperties, useState } from 'react';
|
||||
|
||||
import type {
|
||||
JumpHopDraftResponse,
|
||||
JumpHopPath,
|
||||
JumpHopPlatform,
|
||||
JumpHopTileAsset,
|
||||
JumpHopWorkProfileResponse,
|
||||
} from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import {
|
||||
formatJumpHopDurationLabel,
|
||||
selectJumpHopTileAsset,
|
||||
} from '../../services/jump-hop/jumpHopRuntimeModel';
|
||||
import { useJumpHopLeaderboard } from '../../services/jump-hop/useJumpHopLeaderboard';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
|
||||
type JumpHopResultViewProps = {
|
||||
@@ -34,7 +38,6 @@ type JumpHopResultViewProps = {
|
||||
onEdit: () => void;
|
||||
onStartTestRun: () => void;
|
||||
onPublish: () => void;
|
||||
onRegenerateCharacter: () => void;
|
||||
onRegenerateTiles: () => void;
|
||||
};
|
||||
|
||||
@@ -44,43 +47,6 @@ function isJumpHopWorkProfile(
|
||||
return 'summary' in profile;
|
||||
}
|
||||
|
||||
type MiniMapPlatform = {
|
||||
platform: JumpHopPlatform;
|
||||
index: number;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
isStart: boolean;
|
||||
isFinish: boolean;
|
||||
};
|
||||
|
||||
const difficultyToneByValue: Record<
|
||||
JumpHopPath['difficulty'],
|
||||
{ accent: string; soft: string; label: string }
|
||||
> = {
|
||||
advanced: {
|
||||
accent: '#df7f40',
|
||||
soft: 'rgba(249, 115, 22, 0.16)',
|
||||
label: '进阶',
|
||||
},
|
||||
challenge: {
|
||||
accent: '#b64a35',
|
||||
soft: 'rgba(182, 98, 63, 0.16)',
|
||||
label: '挑战',
|
||||
},
|
||||
easy: {
|
||||
accent: '#14b8a6',
|
||||
soft: 'rgba(20, 184, 166, 0.16)',
|
||||
label: '轻松',
|
||||
},
|
||||
standard: {
|
||||
accent: '#2563eb',
|
||||
soft: 'rgba(37, 99, 235, 0.16)',
|
||||
label: '标准',
|
||||
},
|
||||
};
|
||||
|
||||
const tileToneByType: Record<string, string> = {
|
||||
accent: '#c4b5fd',
|
||||
bonus: '#fde68a',
|
||||
@@ -90,155 +56,191 @@ const tileToneByType: Record<string, string> = {
|
||||
target: '#fecdd3',
|
||||
};
|
||||
|
||||
function isFiniteNumber(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value);
|
||||
const JUMP_HOP_TAONIER_CHARACTER_IMAGE_SRC =
|
||||
'/branding/jump-hop-taonier-character.png';
|
||||
|
||||
function JumpHopDefaultCharacterPreview() {
|
||||
return (
|
||||
<div className="relative grid aspect-[1/1] place-items-center overflow-hidden bg-[linear-gradient(180deg,#eff6ff_0%,#fff7ed_100%)]">
|
||||
<div className="absolute inset-x-[18%] bottom-[14%] h-[14%] rounded-full bg-slate-900/12 blur-[2px]" />
|
||||
<img
|
||||
src={JUMP_HOP_TAONIER_CHARACTER_IMAGE_SRC}
|
||||
alt=""
|
||||
draggable={false}
|
||||
className="relative z-10 h-[78%] w-[78%] object-contain drop-shadow-[0_12px_18px_rgba(146,64,14,0.2)]"
|
||||
data-testid="jump-hop-result-character-logo"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function normalizePathPlatforms(path: JumpHopPath | null | undefined) {
|
||||
const platforms = path?.platforms ?? [];
|
||||
if (platforms.length === 0) {
|
||||
return [];
|
||||
function JumpHopTilePoolPreview({
|
||||
tileAssets,
|
||||
tileAtlasAsset,
|
||||
tileAtlasFallbackSrc,
|
||||
}: {
|
||||
tileAssets: JumpHopTileAsset[];
|
||||
tileAtlasAsset?: JumpHopDraftResponse['tileAtlasAsset'] | null;
|
||||
tileAtlasFallbackSrc?: string | null;
|
||||
}) {
|
||||
const visibleTiles = tileAssets.slice(0, 25);
|
||||
const atlasSrc =
|
||||
tileAtlasAsset?.imageSrc?.trim() || tileAtlasFallbackSrc?.trim() || '';
|
||||
const atlasRefreshKey = tileAtlasAsset?.assetObjectId || atlasSrc;
|
||||
if (visibleTiles.length > 0) {
|
||||
return (
|
||||
<div className="grid aspect-[1/1] grid-cols-5 gap-1 bg-white/78 p-2">
|
||||
{visibleTiles.map((tile, index) => (
|
||||
<div
|
||||
key={tile.tileId ?? `${tile.sourceAtlasCell}-${index}`}
|
||||
className="grid min-h-0 place-items-center overflow-hidden rounded-[0.45rem] border border-white/80 bg-slate-50"
|
||||
>
|
||||
{tile.imageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={tile.imageSrc}
|
||||
refreshKey={tile.assetObjectId}
|
||||
alt=""
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="h-4 w-4 rounded-full"
|
||||
style={{
|
||||
background:
|
||||
tileToneByType[tile.tileType] ?? tileToneByType.normal,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const coordinatePlatforms = platforms.filter(
|
||||
(platform) => isFiniteNumber(platform.x) && isFiniteNumber(platform.y),
|
||||
);
|
||||
const shouldUseCoordinates = coordinatePlatforms.length >= 2;
|
||||
const xValues = shouldUseCoordinates
|
||||
? coordinatePlatforms.map((platform) => platform.x)
|
||||
: [];
|
||||
const yValues = shouldUseCoordinates
|
||||
? coordinatePlatforms.map((platform) => platform.y)
|
||||
: [];
|
||||
const minX = Math.min(...xValues);
|
||||
const maxX = Math.max(...xValues);
|
||||
const minY = Math.min(...yValues);
|
||||
const maxY = Math.max(...yValues);
|
||||
const xRange = Math.max(maxX - minX, 1);
|
||||
const yRange = Math.max(maxY - minY, 1);
|
||||
const denominator = Math.max(platforms.length - 1, 1);
|
||||
|
||||
return platforms.map((platform, index): MiniMapPlatform => {
|
||||
const sequenceRatio = index / denominator;
|
||||
const hasCoordinates =
|
||||
shouldUseCoordinates &&
|
||||
isFiniteNumber(platform.x) &&
|
||||
isFiniteNumber(platform.y);
|
||||
const x = hasCoordinates
|
||||
? 12 + ((platform.x - minX) / xRange) * 76
|
||||
: 12 + sequenceRatio * 76;
|
||||
const y = hasCoordinates
|
||||
? 14 + ((platform.y - minY) / yRange) * 72
|
||||
: 50 + Math.sin(sequenceRatio * Math.PI * 2.3) * 18;
|
||||
|
||||
return {
|
||||
platform,
|
||||
index,
|
||||
x,
|
||||
y,
|
||||
width: Math.min(Math.max(platform.width || 54, 42), 82),
|
||||
height: Math.min(Math.max(platform.height || 42, 34), 68),
|
||||
isStart: index === 0 || platform.tileType === 'start',
|
||||
isFinish:
|
||||
index === path?.finishIndex ||
|
||||
platform.tileType === 'finish' ||
|
||||
platform.tileType === 'target',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function JumpHopPathMiniMap({ path }: { path: JumpHopPath }) {
|
||||
const platforms = useMemo(() => normalizePathPlatforms(path), [path]);
|
||||
const tone =
|
||||
difficultyToneByValue[path.difficulty] ?? difficultyToneByValue.standard;
|
||||
const pathPoints = platforms
|
||||
.map((platform) => `${platform.x},${platform.y}`)
|
||||
.join(' ');
|
||||
|
||||
if (platforms.length === 0) {
|
||||
return null;
|
||||
if (atlasSrc) {
|
||||
return (
|
||||
<ResolvedAssetImage
|
||||
src={atlasSrc}
|
||||
refreshKey={atlasRefreshKey}
|
||||
alt=""
|
||||
className="aspect-[1/1] w-full object-cover"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative aspect-[1/1] w-full overflow-hidden bg-[linear-gradient(180deg,#f8fbff_0%,#eef8ff_100%)]"
|
||||
style={
|
||||
{
|
||||
'--jump-hop-path-accent': tone.accent,
|
||||
'--jump-hop-path-soft': tone.soft,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_24%_18%,rgba(255,255,255,0.92),transparent_28%),radial-gradient(circle_at_75%_78%,rgba(125,211,252,0.24),transparent_32%)]" />
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
className="absolute inset-0 h-full w-full"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<polyline
|
||||
points={pathPoints}
|
||||
fill="none"
|
||||
stroke="var(--jump-hop-path-soft)"
|
||||
strokeWidth="11"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
<div className="grid aspect-[1/1] grid-cols-5 gap-1 bg-white/78 p-2">
|
||||
{Array.from({ length: 25 }).map((_, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="rounded-[0.45rem] border border-white/80"
|
||||
style={{
|
||||
background:
|
||||
Object.values(tileToneByType)[index % Object.values(tileToneByType).length],
|
||||
}}
|
||||
/>
|
||||
<polyline
|
||||
points={pathPoints}
|
||||
fill="none"
|
||||
stroke="var(--jump-hop-path-accent)"
|
||||
strokeWidth="2.6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeDasharray="4 4"
|
||||
/>
|
||||
</svg>
|
||||
{platforms.map((item) => {
|
||||
const tileTone =
|
||||
tileToneByType[item.platform.tileType] ?? tileToneByType.normal;
|
||||
const scoreBoost =
|
||||
isFiniteNumber(item.platform.scoreValue) &&
|
||||
item.platform.scoreValue > 1;
|
||||
const style = {
|
||||
left: `${item.x}%`,
|
||||
top: `${item.y}%`,
|
||||
width: `${item.width}%`,
|
||||
height: `${item.height}%`,
|
||||
background: tileTone,
|
||||
borderColor: item.isFinish ? tone.accent : 'rgba(255,255,255,0.92)',
|
||||
zIndex: 10 + item.index,
|
||||
} as CSSProperties;
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function JumpHopFirstPlatformsPreview({
|
||||
path,
|
||||
tileAssets,
|
||||
}: {
|
||||
path: JumpHopPath | null | undefined;
|
||||
tileAssets: JumpHopTileAsset[];
|
||||
}) {
|
||||
const platforms = (path?.platforms ?? []).slice(0, 3);
|
||||
|
||||
return (
|
||||
<div className="relative aspect-[1/1] overflow-hidden bg-[linear-gradient(180deg,#f8fbff_0%,#eef8ff_100%)]">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_30%,rgba(255,255,255,0.92),transparent_34%)]" />
|
||||
{platforms.map((platform, index) => {
|
||||
const asset = selectJumpHopTileAsset(
|
||||
tileAssets,
|
||||
path?.seed,
|
||||
index,
|
||||
platform.platformId,
|
||||
);
|
||||
const style = {
|
||||
left: `${50 + (index - 1) * 24}%`,
|
||||
top: `${68 - index * 22}%`,
|
||||
width: `${34 - index * 3}%`,
|
||||
zIndex: 10 + index,
|
||||
} as CSSProperties;
|
||||
return (
|
||||
<div
|
||||
key={
|
||||
item.platform.platformId ||
|
||||
`${item.index}-${item.platform.tileType}`
|
||||
}
|
||||
className="absolute grid max-h-9 max-w-11 min-h-6 min-w-7 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-[0.72rem] border-2 shadow-[0_8px_18px_rgba(15,23,42,0.13)]"
|
||||
key={platform.platformId || index}
|
||||
className="absolute aspect-[1.16/1] -translate-x-1/2 -translate-y-1/2"
|
||||
style={style}
|
||||
>
|
||||
<span
|
||||
className="h-2.5 w-2.5 rounded-full"
|
||||
style={{
|
||||
background:
|
||||
item.isStart || item.isFinish ? tone.accent : '#ffffff',
|
||||
boxShadow: scoreBoost ? `0 0 0 4px ${tone.soft}` : undefined,
|
||||
}}
|
||||
/>
|
||||
{item.isStart || item.isFinish ? (
|
||||
<span className="absolute -top-2.5 rounded-full bg-slate-950/78 px-1.5 py-0.5 text-[0.58rem] font-black leading-none text-white">
|
||||
{item.isStart ? '起' : '终'}
|
||||
</span>
|
||||
) : null}
|
||||
<div className="absolute inset-x-[12%] bottom-[-6%] h-[22%] rounded-full bg-slate-900/14 blur-[3px]" />
|
||||
{asset?.imageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={asset.imageSrc}
|
||||
refreshKey={asset.assetObjectId}
|
||||
alt=""
|
||||
className="relative h-full w-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="relative h-full w-full rounded-[18%] border-2 border-white/90 shadow-[0_10px_22px_rgba(15,23,42,0.14)]"
|
||||
style={{
|
||||
background:
|
||||
tileToneByType[platform.tileType] ?? tileToneByType.normal,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="absolute left-2 top-2 rounded-full border border-white/80 bg-white/82 px-2 py-1 text-[0.62rem] font-black text-[var(--platform-text-strong)] shadow-sm">
|
||||
{tone.label}
|
||||
{platforms.length === 0 ? (
|
||||
<div className="absolute inset-0 grid place-items-center text-sm font-bold text-[var(--platform-text-soft)]">
|
||||
路径
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function JumpHopResultLeaderboard({
|
||||
profileId,
|
||||
}: {
|
||||
profileId?: string | null;
|
||||
}) {
|
||||
const { leaderboard, isLoading, error } = useJumpHopLeaderboard(profileId);
|
||||
const items = leaderboard?.items ?? [];
|
||||
|
||||
return (
|
||||
<div className="mt-4 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/70 p-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="text-sm font-black text-[var(--platform-text-strong)]">
|
||||
排行榜
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-[var(--platform-text-soft)]" />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="absolute bottom-2 right-2 rounded-full border border-white/80 bg-white/82 px-2 py-1 text-[0.62rem] font-black text-[var(--platform-text-base)] shadow-sm">
|
||||
{platforms.length}
|
||||
<div className="mt-3 grid gap-2">
|
||||
{items.slice(0, 5).map((entry) => (
|
||||
<div
|
||||
key={`${entry.rank}-${entry.playerId}`}
|
||||
className="grid grid-cols-[1.8rem_minmax(0,1fr)_auto_auto] items-center gap-2 rounded-[0.75rem] bg-white/70 px-2 py-2 text-xs font-bold text-[var(--platform-text-base)]"
|
||||
>
|
||||
<span className="text-[var(--platform-text-soft)]">
|
||||
{entry.rank}
|
||||
</span>
|
||||
<span className="truncate">{entry.playerId}</span>
|
||||
<span>{entry.successfulJumpCount} 跳</span>
|
||||
<span>{formatJumpHopDurationLabel(entry.durationMs)}</span>
|
||||
</div>
|
||||
))}
|
||||
{items.length === 0 ? (
|
||||
<div className="rounded-[0.75rem] bg-white/60 px-2 py-2 text-xs font-bold text-[var(--platform-text-soft)]">
|
||||
{error ?? '暂无成绩'}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -252,7 +254,6 @@ export function JumpHopResultView({
|
||||
onEdit,
|
||||
onStartTestRun,
|
||||
onPublish,
|
||||
onRegenerateCharacter,
|
||||
onRegenerateTiles,
|
||||
}: JumpHopResultViewProps) {
|
||||
const [isPublishing, setIsPublishing] = useState(false);
|
||||
@@ -264,12 +265,15 @@ export function JumpHopResultView({
|
||||
path: NonNullable<JumpHopDraftResponse['path']>;
|
||||
};
|
||||
const path = isWorkProfile ? profile.path : safeDraft.path;
|
||||
const characterAsset = isWorkProfile
|
||||
? profile.characterAsset
|
||||
: safeDraft.characterAsset;
|
||||
const tileAtlasAsset = isWorkProfile
|
||||
? profile.tileAtlasAsset
|
||||
: safeDraft.tileAtlasAsset;
|
||||
const tileAssets = isWorkProfile ? profile.tileAssets : safeDraft.tileAssets;
|
||||
const profileId = isWorkProfile
|
||||
? profile.summary.profileId
|
||||
: safeDraft.profileId;
|
||||
const canShowLeaderboard =
|
||||
isWorkProfile && profile.summary.publicationStatus === 'published';
|
||||
const titleSource = isWorkProfile
|
||||
? profile.summary.workTitle
|
||||
: profile.workTitle;
|
||||
@@ -278,15 +282,12 @@ export function JumpHopResultView({
|
||||
: profile.workDescription;
|
||||
const title = titleSource?.trim() || safeDraft.workTitle.trim() || '跳一跳';
|
||||
const summary = summarySource?.trim() || safeDraft.workDescription.trim();
|
||||
const pathPlatforms = normalizePathPlatforms(path);
|
||||
const canRenderPathMiniMap = pathPlatforms.length > 0;
|
||||
const hasAssets = Boolean(
|
||||
profile.characterImageSrc?.trim() ||
|
||||
profile.tileAtlasImageSrc?.trim() ||
|
||||
profile.tileAtlasImageSrc?.trim() ||
|
||||
profile.pathPreviewImageSrc?.trim() ||
|
||||
characterAsset?.imageSrc?.trim() ||
|
||||
tileAtlasAsset?.imageSrc?.trim() ||
|
||||
canRenderPathMiniMap,
|
||||
tileAssets.length > 0 ||
|
||||
path?.platforms.length,
|
||||
);
|
||||
|
||||
const handlePublish = async () => {
|
||||
@@ -310,15 +311,6 @@ export function JumpHopResultView({
|
||||
返回
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRegenerateCharacter}
|
||||
disabled={isBusy}
|
||||
className="platform-button platform-button--ghost min-h-0 px-3 py-2 text-sm"
|
||||
>
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
角色
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRegenerateTiles}
|
||||
@@ -343,69 +335,25 @@ export function JumpHopResultView({
|
||||
) : null}
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-3">
|
||||
<div className="overflow-hidden rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/80">
|
||||
{profile.characterImageSrc || characterAsset?.imageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={
|
||||
('characterImageSrc' in profile
|
||||
? profile.characterImageSrc
|
||||
: null) ??
|
||||
characterAsset?.imageSrc ??
|
||||
''
|
||||
}
|
||||
alt="角色图"
|
||||
className="aspect-[1/1] w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid aspect-[1/1] place-items-center text-sm text-[var(--platform-text-soft)]">
|
||||
角色
|
||||
</div>
|
||||
)}
|
||||
<JumpHopDefaultCharacterPreview />
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/80">
|
||||
{profile.tileAtlasImageSrc || tileAtlasAsset?.imageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={
|
||||
('tileAtlasImageSrc' in profile
|
||||
? profile.tileAtlasImageSrc
|
||||
: null) ??
|
||||
tileAtlasAsset?.imageSrc ??
|
||||
''
|
||||
}
|
||||
alt="地块图"
|
||||
className="aspect-[1/1] w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid aspect-[1/1] place-items-center text-sm text-[var(--platform-text-soft)]">
|
||||
地块
|
||||
</div>
|
||||
)}
|
||||
<JumpHopTilePoolPreview
|
||||
tileAssets={tileAssets}
|
||||
tileAtlasAsset={tileAtlasAsset}
|
||||
tileAtlasFallbackSrc={
|
||||
('tileAtlasImageSrc' in profile
|
||||
? profile.tileAtlasImageSrc
|
||||
: null) ??
|
||||
null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/80">
|
||||
{path && canRenderPathMiniMap ? (
|
||||
<JumpHopPathMiniMap path={path} />
|
||||
) : 'pathPreviewImageSrc' in profile &&
|
||||
profile.pathPreviewImageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={profile.pathPreviewImageSrc}
|
||||
alt="路径预览"
|
||||
className="aspect-[1/1] w-full object-cover"
|
||||
/>
|
||||
) : path ? (
|
||||
<div className="grid aspect-[1/1] place-items-center px-3 text-center">
|
||||
<div>
|
||||
<div className="text-3xl font-black text-[var(--platform-text-strong)]">
|
||||
{path.platforms.length}
|
||||
</div>
|
||||
<div className="mt-1 text-xs font-bold tracking-[0.16em] text-[var(--platform-text-soft)]">
|
||||
{path.difficulty}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid aspect-[1/1] place-items-center text-sm text-[var(--platform-text-soft)]">
|
||||
路径
|
||||
</div>
|
||||
)}
|
||||
<JumpHopFirstPlatformsPreview
|
||||
path={path}
|
||||
tileAssets={tileAssets}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!hasAssets ? (
|
||||
@@ -419,6 +367,9 @@ export function JumpHopResultView({
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
结果操作
|
||||
</div>
|
||||
{canShowLeaderboard ? (
|
||||
<JumpHopResultLeaderboard profileId={profileId} />
|
||||
) : null}
|
||||
{error ? (
|
||||
<div className="platform-banner platform-banner--danger mt-3 rounded-2xl text-sm leading-6">
|
||||
{error}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
resolveMiniGameGenerationProgressTickState,
|
||||
} from './PlatformEntryFlowShellImpl';
|
||||
import { createMiniGameDraftGenerationState } from '../../services/miniGameDraftGenerationProgress';
|
||||
|
||||
describe('resolveMiniGameGenerationProgressTickState', () => {
|
||||
test('returns jump hop and wooden fish generation states for progress ticking', () => {
|
||||
const jumpHopState = createMiniGameDraftGenerationState('jump-hop');
|
||||
const woodenFishState = createMiniGameDraftGenerationState('wooden-fish');
|
||||
|
||||
expect(
|
||||
resolveMiniGameGenerationProgressTickState('jump-hop-generating', {
|
||||
'jump-hop': jumpHopState,
|
||||
}),
|
||||
).toBe(jumpHopState);
|
||||
expect(
|
||||
resolveMiniGameGenerationProgressTickState('wooden-fish-generating', {
|
||||
'wooden-fish': woodenFishState,
|
||||
}),
|
||||
).toBe(woodenFishState);
|
||||
});
|
||||
|
||||
test('returns null when the stage does not need generation ticking', () => {
|
||||
expect(
|
||||
resolveMiniGameGenerationProgressTickState('platform', {
|
||||
'jump-hop': createMiniGameDraftGenerationState('jump-hop'),
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -38,7 +38,10 @@ import type {
|
||||
BabyObjectMatchDraft,
|
||||
CreateBabyObjectMatchDraftRequest,
|
||||
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import type {
|
||||
JumpHopJumpRequest,
|
||||
JumpHopWorkSummaryResponse,
|
||||
} from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import type {
|
||||
CreateMatch3DSessionRequest,
|
||||
ExecuteMatch3DActionRequest,
|
||||
@@ -109,6 +112,7 @@ import type {
|
||||
VisualNovelWorkDetail,
|
||||
VisualNovelWorkSummary,
|
||||
} from '../../../packages/shared/src/contracts/visualNovel';
|
||||
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
|
||||
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
||||
import {
|
||||
buildPublicWorkStagePath,
|
||||
@@ -189,6 +193,7 @@ import {
|
||||
jumpHopClient,
|
||||
type JumpHopGalleryCardResponse,
|
||||
type JumpHopRunResponse,
|
||||
type JumpHopRuntimeRequestOptions,
|
||||
type JumpHopSessionResponse,
|
||||
type JumpHopSessionSnapshotResponse,
|
||||
JumpHopWorkProfileResponse,
|
||||
@@ -352,7 +357,6 @@ import {
|
||||
type WoodenFishWorkProfileResponse,
|
||||
type WoodenFishWorkspaceCreateRequest,
|
||||
} from '../../services/wooden-fish/woodenFishClient';
|
||||
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { PublishShareModal } from '../common/PublishShareModal';
|
||||
@@ -441,11 +445,11 @@ import {
|
||||
PlatformErrorDialog,
|
||||
type PlatformErrorDialogPayload,
|
||||
} from './PlatformErrorDialog';
|
||||
import { PlatformFeedbackView } from './PlatformFeedbackView';
|
||||
import {
|
||||
PlatformTaskCompletionDialog,
|
||||
type PlatformTaskCompletionDialogPayload,
|
||||
} from './PlatformTaskCompletionDialog';
|
||||
import { PlatformFeedbackView } from './PlatformFeedbackView';
|
||||
import { PlatformWorkDetailView } from './PlatformWorkDetailView';
|
||||
import { usePlatformCreationAgentFlowController } from './usePlatformCreationAgentFlowController';
|
||||
import { usePlatformEntryBootstrap } from './usePlatformEntryBootstrap';
|
||||
@@ -492,6 +496,30 @@ type PuzzleBackgroundCompileTask = {
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
type MiniGameGenerationProgressTickStateMap = Partial<
|
||||
Record<MiniGameDraftGenerationKind, MiniGameDraftGenerationState | null>
|
||||
>;
|
||||
|
||||
export function resolveMiniGameGenerationProgressTickState(
|
||||
selectionStage: SelectionStage,
|
||||
states: MiniGameGenerationProgressTickStateMap,
|
||||
) {
|
||||
const stageKindMap: Partial<
|
||||
Record<SelectionStage, MiniGameDraftGenerationKind>
|
||||
> = {
|
||||
'puzzle-generating': 'puzzle',
|
||||
'big-fish-generating': 'big-fish',
|
||||
'square-hole-generating': 'square-hole',
|
||||
'match3d-generating': 'match3d',
|
||||
'baby-object-match-generating': 'baby-object-match',
|
||||
'jump-hop-generating': 'jump-hop',
|
||||
'wooden-fish-generating': 'wooden-fish',
|
||||
};
|
||||
const kind = stageKindMap[selectionStage];
|
||||
|
||||
return kind ? (states[kind] ?? null) : null;
|
||||
}
|
||||
|
||||
type PuzzleDetailReturnTarget = {
|
||||
tab: PlatformHomeTab;
|
||||
};
|
||||
@@ -595,11 +623,11 @@ const AGENT_RESULT_STRUCTURAL_BLOCKER_CODES = new Set([
|
||||
'publish_missing_main_chapter',
|
||||
'publish_missing_first_act',
|
||||
]);
|
||||
const RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS =
|
||||
const RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS: JumpHopRuntimeRequestOptions =
|
||||
BACKGROUND_AUTH_REQUEST_OPTIONS;
|
||||
const RECOMMEND_PUZZLE_BACKGROUND_AUTH_OPTIONS =
|
||||
const RECOMMEND_PUZZLE_BACKGROUND_AUTH_OPTIONS: JumpHopRuntimeRequestOptions =
|
||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
|
||||
async function buildRecommendRuntimeGuestOptions() {
|
||||
async function buildRecommendRuntimeGuestOptions(): Promise<JumpHopRuntimeRequestOptions> {
|
||||
const { token } = await ensureRuntimeGuestToken();
|
||||
return {
|
||||
...RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||||
@@ -614,9 +642,9 @@ function shouldUseRecommendRuntimeGuestAuth(
|
||||
async function buildRecommendRuntimeAuthOptions(
|
||||
authUi: { user?: { id?: string } | null } | null | undefined,
|
||||
embedded?: boolean,
|
||||
) {
|
||||
): Promise<JumpHopRuntimeRequestOptions> {
|
||||
if (!embedded) {
|
||||
return {};
|
||||
return RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
|
||||
}
|
||||
|
||||
if (shouldUseRecommendRuntimeGuestAuth(authUi)) {
|
||||
@@ -1967,6 +1995,7 @@ function buildJumpHopPendingSession(
|
||||
templateId: 'jump-hop',
|
||||
templateName: '跳一跳',
|
||||
profileId: item.profileId,
|
||||
themeText: item.themeText || item.workTitle,
|
||||
workTitle: item.workTitle,
|
||||
workDescription: item.workDescription,
|
||||
themeTags: item.themeTags,
|
||||
@@ -2773,6 +2802,7 @@ function buildPendingJumpHopWorks(
|
||||
profileId: `jump-hop-profile-${sessionId}`,
|
||||
ownerUserId: '',
|
||||
sourceSessionId: sessionId,
|
||||
themeText: '跳一跳',
|
||||
workTitle: '跳一跳草稿',
|
||||
workDescription:
|
||||
state.status === 'failed'
|
||||
@@ -3596,6 +3626,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
const [jumpHopRun, setJumpHopRun] = useState<
|
||||
JumpHopRunResponse['run'] | null
|
||||
>(null);
|
||||
const [jumpHopRuntimeRequestOptions, setJumpHopRuntimeRequestOptions] =
|
||||
useState<JumpHopRuntimeRequestOptions | null>(null);
|
||||
const [jumpHopWork, setJumpHopWork] =
|
||||
useState<JumpHopWorkProfileResponse | null>(null);
|
||||
const [jumpHopGalleryEntries, setJumpHopGalleryEntries] = useState<
|
||||
@@ -5375,22 +5407,18 @@ export function PlatformEntryFlowShellImpl({
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const activeGenerationState =
|
||||
selectionStage === 'puzzle-generating'
|
||||
? puzzleGenerationState
|
||||
: selectionStage === 'match3d-generating'
|
||||
? match3dGenerationState
|
||||
: selectionStage === 'big-fish-generating'
|
||||
? bigFishGenerationState
|
||||
: selectionStage === 'square-hole-generating'
|
||||
? squareHoleGenerationState
|
||||
: selectionStage === 'jump-hop-generating'
|
||||
? jumpHopGenerationState
|
||||
: selectionStage === 'wooden-fish-generating'
|
||||
? woodenFishGenerationState
|
||||
: selectionStage === 'baby-object-match-generating'
|
||||
? babyObjectMatchGenerationState
|
||||
: null;
|
||||
const activeGenerationState = resolveMiniGameGenerationProgressTickState(
|
||||
selectionStage,
|
||||
{
|
||||
puzzle: puzzleGenerationState,
|
||||
'big-fish': bigFishGenerationState,
|
||||
'square-hole': squareHoleGenerationState,
|
||||
match3d: match3dGenerationState,
|
||||
'baby-object-match': babyObjectMatchGenerationState,
|
||||
'jump-hop': jumpHopGenerationState,
|
||||
'wooden-fish': woodenFishGenerationState,
|
||||
},
|
||||
);
|
||||
const shouldTickProgress =
|
||||
selectionStage === 'visual-novel-generating'
|
||||
? visualNovelGenerationStartedAtMs != null &&
|
||||
@@ -7384,6 +7412,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setJumpHopSession(null);
|
||||
setJumpHopWork(null);
|
||||
setJumpHopRun(null);
|
||||
setJumpHopRuntimeRequestOptions(null);
|
||||
setJumpHopGenerationState(null);
|
||||
enterCreateTab();
|
||||
setShowCreationTypeModal(false);
|
||||
@@ -8516,6 +8545,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setJumpHopRuntimeReturnStage('jump-hop-result');
|
||||
setJumpHopGenerationState(null);
|
||||
setJumpHopSession(null);
|
||||
setJumpHopRuntimeRequestOptions(null);
|
||||
setJumpHopError(null);
|
||||
returnToCreationFlowSource();
|
||||
}, [returnToCreationFlowSource]);
|
||||
@@ -9545,6 +9575,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
setJumpHopWork(null);
|
||||
setJumpHopRun(null);
|
||||
setJumpHopRuntimeRequestOptions(null);
|
||||
setJumpHopGenerationState(generationState);
|
||||
setIsJumpHopBusy(true);
|
||||
setSelectionStage('jump-hop-generating');
|
||||
@@ -9559,6 +9590,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
created.session.sessionId,
|
||||
{
|
||||
actionType: 'compile-draft',
|
||||
themeText:
|
||||
payload?.themeText ?? created.session.draft?.themeText,
|
||||
workTitle: payload?.workTitle ?? created.session.draft?.workTitle,
|
||||
workDescription:
|
||||
payload?.workDescription ??
|
||||
@@ -9673,7 +9706,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}, [compileJumpHopSession, jumpHopSession, setSelectionStage]);
|
||||
|
||||
const regenerateJumpHopAsset = useCallback(
|
||||
async (actionType: 'regenerate-character' | 'regenerate-tiles') => {
|
||||
async (actionType: 'regenerate-tiles') => {
|
||||
if (!jumpHopSession?.sessionId) {
|
||||
setSelectionStage('jump-hop-workspace');
|
||||
return;
|
||||
@@ -9689,6 +9722,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
jumpHopSession.sessionId,
|
||||
{
|
||||
actionType,
|
||||
profileId:
|
||||
jumpHopWork?.summary.profileId ?? jumpHopSession.draft?.profileId,
|
||||
themeText: jumpHopSession.draft?.themeText,
|
||||
workTitle: jumpHopSession.draft?.workTitle,
|
||||
workDescription: jumpHopSession.draft?.workDescription,
|
||||
themeTags: jumpHopSession.draft?.themeTags,
|
||||
@@ -9714,9 +9750,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
} catch (error) {
|
||||
const errorMessage = resolveRpgCreationErrorMessage(
|
||||
error,
|
||||
actionType === 'regenerate-character'
|
||||
? '重新生成跳一跳角色失败。'
|
||||
: '重新生成跳一跳地块失败。',
|
||||
'重新生成跳一跳地块失败。',
|
||||
);
|
||||
setJumpHopError(errorMessage);
|
||||
setJumpHopGenerationState(
|
||||
@@ -9792,7 +9826,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
setJumpHopError(null);
|
||||
setJumpHopRuntimeReturnStage('jump-hop-result');
|
||||
try {
|
||||
const response = await jumpHopClient.startRun(profileId);
|
||||
const response = await jumpHopClient.startRun(profileId, {
|
||||
runtimeMode: 'draft',
|
||||
});
|
||||
setJumpHopRun(response.run);
|
||||
setSelectionStage('jump-hop-runtime');
|
||||
} catch (error) {
|
||||
@@ -9823,13 +9859,30 @@ export function PlatformEntryFlowShellImpl({
|
||||
setJumpHopError(null);
|
||||
setJumpHopRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
||||
try {
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
const runtimeGuestOptions =
|
||||
options.embedded || shouldUseRecommendRuntimeGuestAuth(authUi)
|
||||
? await buildRecommendRuntimeAuthOptions(authUi, true)
|
||||
: RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
|
||||
setJumpHopRuntimeRequestOptions(
|
||||
runtimeGuestOptions.runtimeGuestToken?.trim()
|
||||
? {
|
||||
runtimeGuestToken: runtimeGuestOptions.runtimeGuestToken,
|
||||
authImpact: runtimeGuestOptions.authImpact,
|
||||
skipAuth: runtimeGuestOptions.skipAuth,
|
||||
skipRefresh: runtimeGuestOptions.skipRefresh,
|
||||
notifyAuthStateChange:
|
||||
runtimeGuestOptions.notifyAuthStateChange,
|
||||
clearAuthOnUnauthorized:
|
||||
runtimeGuestOptions.clearAuthOnUnauthorized,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
const [detail, runResponse] = await Promise.all([
|
||||
jumpHopClient.getWorkDetail(normalizedProfileId).catch(() => null),
|
||||
jumpHopClient.startRun(normalizedProfileId, runtimeGuestOptions),
|
||||
jumpHopClient.startRun(normalizedProfileId, {
|
||||
...runtimeGuestOptions,
|
||||
runtimeMode: 'published',
|
||||
}),
|
||||
]);
|
||||
if (detail?.item) {
|
||||
setJumpHopWork(detail.item);
|
||||
@@ -9867,7 +9920,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsJumpHopBusy(true);
|
||||
setJumpHopError(null);
|
||||
try {
|
||||
const response = await jumpHopClient.restartRun(runId);
|
||||
const response = await jumpHopClient.restartRun(
|
||||
runId,
|
||||
jumpHopRuntimeRequestOptions ?? undefined,
|
||||
);
|
||||
setJumpHopRun(response.run);
|
||||
} catch (error) {
|
||||
setJumpHopError(
|
||||
@@ -9876,16 +9932,29 @@ export function PlatformEntryFlowShellImpl({
|
||||
} finally {
|
||||
setIsJumpHopBusy(false);
|
||||
}
|
||||
}, [jumpHopRun?.runId, startJumpHopTestRunFromProfile]);
|
||||
}, [
|
||||
jumpHopRun?.runId,
|
||||
jumpHopRuntimeRequestOptions,
|
||||
startJumpHopTestRunFromProfile,
|
||||
]);
|
||||
|
||||
const submitJumpHopJumpAction = useCallback(
|
||||
async (payload: { chargeMs: number }) => {
|
||||
async (
|
||||
payload: Pick<
|
||||
JumpHopJumpRequest,
|
||||
'dragDistance' | 'dragVectorX' | 'dragVectorY'
|
||||
>,
|
||||
) => {
|
||||
const runId = jumpHopRun?.runId;
|
||||
if (!runId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await jumpHopClient.submitJump(runId, payload);
|
||||
const response = await jumpHopClient.submitJump(
|
||||
runId,
|
||||
payload,
|
||||
jumpHopRuntimeRequestOptions ?? undefined,
|
||||
);
|
||||
setJumpHopRun(response.run);
|
||||
} catch (error) {
|
||||
setJumpHopError(
|
||||
@@ -9893,7 +9962,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
}
|
||||
},
|
||||
[jumpHopRun?.runId],
|
||||
[jumpHopRun?.runId, jumpHopRuntimeRequestOptions],
|
||||
);
|
||||
|
||||
const compileWoodenFishSession = useCallback(
|
||||
@@ -14426,6 +14495,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isJumpHopGalleryEntry(selectedPublicWorkDetail)) {
|
||||
setPublicWorkDetailError(null);
|
||||
void startJumpHopRunFromProfile(selectedPublicWorkDetail.profileId, {
|
||||
returnStage: 'work-detail',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
runProtectedAction(() => {
|
||||
if (isBigFishGalleryEntry(selectedPublicWorkDetail)) {
|
||||
const work = mapPublicWorkDetailToBigFishWork(selectedPublicWorkDetail);
|
||||
@@ -14460,14 +14537,6 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isJumpHopGalleryEntry(selectedPublicWorkDetail)) {
|
||||
setPublicWorkDetailError(null);
|
||||
void startJumpHopRunFromProfile(selectedPublicWorkDetail.profileId, {
|
||||
returnStage: 'work-detail',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isWoodenFishGalleryEntry(selectedPublicWorkDetail)) {
|
||||
setPublicWorkDetailError(null);
|
||||
void startWoodenFishRunFromProfile(selectedPublicWorkDetail.profileId, {
|
||||
@@ -14970,37 +15039,15 @@ export function PlatformEntryFlowShellImpl({
|
||||
run={jumpHopRun}
|
||||
isBusy={isJumpHopBusy}
|
||||
error={jumpHopError}
|
||||
runtimeRequestOptions={jumpHopRuntimeRequestOptions ?? undefined}
|
||||
onBack={() => {
|
||||
setActiveRecommendRuntimeKind(null);
|
||||
}}
|
||||
onRestart={() => {
|
||||
if (!jumpHopRun?.runId || isJumpHopBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsJumpHopBusy(true);
|
||||
setJumpHopError(null);
|
||||
void jumpHopClient
|
||||
.restartRun(jumpHopRun.runId)
|
||||
.then((response) => {
|
||||
setJumpHopRun(response.run);
|
||||
})
|
||||
.catch((error) => {
|
||||
setJumpHopError(
|
||||
resolveRpgCreationErrorMessage(error, '重新开始跳一跳失败。'),
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsJumpHopBusy(false);
|
||||
});
|
||||
void restartJumpHopRuntimeRun();
|
||||
}}
|
||||
onJump={async (payload) => {
|
||||
const runId = jumpHopRun?.runId;
|
||||
if (!runId) {
|
||||
throw new Error('跳一跳运行态缺少 runId。');
|
||||
}
|
||||
const response = await jumpHopClient.submitJump(runId, payload);
|
||||
setJumpHopRun(response.run);
|
||||
await submitJumpHopJumpAction(payload);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -16969,6 +17016,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
)}
|
||||
progress={buildMiniGameDraftGenerationProgress(
|
||||
bigFishGenerationState,
|
||||
miniGameGenerationProgressNowMs,
|
||||
)}
|
||||
isGenerating={isBigFishBusy}
|
||||
error={bigFishError}
|
||||
@@ -17577,6 +17625,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
)}
|
||||
progress={buildMiniGameDraftGenerationProgress(
|
||||
squareHoleGenerationState,
|
||||
miniGameGenerationProgressNowMs,
|
||||
)}
|
||||
isGenerating={isSquareHoleBusy}
|
||||
error={squareHoleError}
|
||||
@@ -17789,6 +17838,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
)}
|
||||
progress={buildMiniGameDraftGenerationProgress(
|
||||
jumpHopGenerationState,
|
||||
miniGameGenerationProgressNowMs,
|
||||
)}
|
||||
isGenerating={isJumpHopBusy}
|
||||
error={jumpHopError}
|
||||
@@ -17830,9 +17880,6 @@ export function PlatformEntryFlowShellImpl({
|
||||
}}
|
||||
onStartTestRun={startJumpHopTestRunFromProfile}
|
||||
onPublish={publishJumpHopDraft}
|
||||
onRegenerateCharacter={() => {
|
||||
void regenerateJumpHopAsset('regenerate-character');
|
||||
}}
|
||||
onRegenerateTiles={() => {
|
||||
void regenerateJumpHopAsset('regenerate-tiles');
|
||||
}}
|
||||
@@ -17868,6 +17915,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
profile={jumpHopWork}
|
||||
isBusy={isJumpHopBusy}
|
||||
error={jumpHopError}
|
||||
runtimeRequestOptions={jumpHopRuntimeRequestOptions ?? undefined}
|
||||
onBack={() => {
|
||||
setSelectionStage(jumpHopRuntimeReturnStage);
|
||||
}}
|
||||
@@ -17929,6 +17977,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
)}
|
||||
progress={buildMiniGameDraftGenerationProgress(
|
||||
woodenFishGenerationState,
|
||||
miniGameGenerationProgressNowMs,
|
||||
)}
|
||||
isGenerating={isWoodenFishBusy}
|
||||
error={woodenFishError}
|
||||
|
||||
@@ -14,14 +14,15 @@ import type {
|
||||
CustomWorldWorkSummary,
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type {
|
||||
BabyObjectMatchDraft,
|
||||
CreateBabyObjectMatchDraftRequest,
|
||||
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import type {
|
||||
JumpHopRuntimeRunSnapshotResponse,
|
||||
JumpHopWorkDetailResponse,
|
||||
JumpHopWorkProfileResponse,
|
||||
JumpHopWorkSummaryResponse,
|
||||
} from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import type {
|
||||
BabyObjectMatchDraft,
|
||||
CreateBabyObjectMatchDraftRequest,
|
||||
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import type { Match3DAgentSessionSnapshot } from '../../../packages/shared/src/contracts/match3dAgent';
|
||||
import type { Match3DRunSnapshot } from '../../../packages/shared/src/contracts/match3dRuntime';
|
||||
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
||||
@@ -70,6 +71,7 @@ import {
|
||||
submitBigFishInput,
|
||||
} from '../../services/big-fish-runtime';
|
||||
import { listBigFishWorks } from '../../services/big-fish-works';
|
||||
import { jumpHopClient } from '../../services/jump-hop/jumpHopClient';
|
||||
import {
|
||||
type CreationEntryConfig,
|
||||
fetchCreationEntryConfig,
|
||||
@@ -89,7 +91,6 @@ import {
|
||||
regenerateBabyObjectMatchDraftAssets,
|
||||
saveBabyObjectMatchDraft,
|
||||
} from '../../services/edutainment-baby-object';
|
||||
import { jumpHopClient } from '../../services/jump-hop/jumpHopClient';
|
||||
import { match3dCreationClient } from '../../services/match3d-creation';
|
||||
import { createServerMatch3DRuntimeAdapter } from '../../services/match3d-runtime';
|
||||
import {
|
||||
@@ -610,6 +611,24 @@ vi.mock('../../services/puzzle-runtime', () => ({
|
||||
usePuzzleRuntimeProp: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/jump-hop/jumpHopClient', () => ({
|
||||
jumpHopClient: {
|
||||
createSession: vi.fn(),
|
||||
deleteWork: vi.fn(),
|
||||
executeAction: vi.fn(),
|
||||
getGalleryDetail: vi.fn(),
|
||||
getLeaderboard: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
getWorkDetail: vi.fn(),
|
||||
listGallery: vi.fn(),
|
||||
listWorks: vi.fn(),
|
||||
publishWork: vi.fn(),
|
||||
restartRun: vi.fn(),
|
||||
startRun: vi.fn(),
|
||||
submitJump: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../services/rpg-entry/rpgEntryLibraryClient', () => ({
|
||||
...rpgEntryLibraryServiceMocks,
|
||||
}));
|
||||
@@ -657,23 +676,6 @@ vi.mock('../../services/edutainment-baby-object', () => ({
|
||||
saveBabyObjectMatchDraft: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/jump-hop/jumpHopClient', () => ({
|
||||
jumpHopClient: {
|
||||
createSession: vi.fn(),
|
||||
deleteWork: vi.fn(),
|
||||
executeAction: vi.fn(),
|
||||
getGalleryDetail: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
getWorkDetail: vi.fn(),
|
||||
listGallery: vi.fn(),
|
||||
listWorks: vi.fn(),
|
||||
publishWork: vi.fn(),
|
||||
restartRun: vi.fn(),
|
||||
startRun: vi.fn(),
|
||||
submitJump: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../services/wooden-fish/woodenFishClient', () => ({
|
||||
woodenFishClient: {
|
||||
checkpointRun: vi.fn(),
|
||||
@@ -684,9 +686,14 @@ vi.mock('../../services/wooden-fish/woodenFishClient', () => ({
|
||||
getGalleryDetail: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
getWorkDetail: vi.fn(),
|
||||
listGallery: vi.fn(),
|
||||
listWorks: vi.fn(),
|
||||
listGallery: vi.fn(async () => ({
|
||||
hasMore: false,
|
||||
items: [],
|
||||
nextCursor: null,
|
||||
})),
|
||||
listWorks: vi.fn(async () => ({ items: [] })),
|
||||
publishWork: vi.fn(),
|
||||
restartRun: vi.fn(),
|
||||
startRun: vi.fn(),
|
||||
},
|
||||
}));
|
||||
@@ -804,23 +811,6 @@ vi.mock('../../services/visual-novel-works', () => ({
|
||||
updateVisualNovelWork: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/wooden-fish/woodenFishClient', () => ({
|
||||
woodenFishClient: {
|
||||
checkpointRun: vi.fn(),
|
||||
createSession: vi.fn(),
|
||||
deleteWork: vi.fn(),
|
||||
executeAction: vi.fn(),
|
||||
finishRun: vi.fn(),
|
||||
getGalleryDetail: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
getWorkDetail: vi.fn(),
|
||||
listGallery: vi.fn(),
|
||||
listWorks: vi.fn(),
|
||||
publishWork: vi.fn(),
|
||||
startRun: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../services/visual-novel-creation', () => ({
|
||||
compileVisualNovelWorkProfile: vi.fn(),
|
||||
createVisualNovelSession: vi.fn(),
|
||||
@@ -1636,6 +1626,7 @@ function buildMockJumpHopWork(
|
||||
templateId: 'jump-hop',
|
||||
templateName: '跳一跳',
|
||||
profileId,
|
||||
themeText: '云朵跳台',
|
||||
workTitle: '云端跳台',
|
||||
workDescription: '一路跳到星星。',
|
||||
themeTags: ['云朵', '星空'],
|
||||
@@ -1649,6 +1640,7 @@ function buildMockJumpHopWork(
|
||||
tileAssets,
|
||||
path,
|
||||
coverComposite: 'data:image/png;base64,cover',
|
||||
backButtonAsset: null,
|
||||
generationStatus: 'ready' as const,
|
||||
};
|
||||
|
||||
@@ -1659,6 +1651,7 @@ function buildMockJumpHopWork(
|
||||
profileId,
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'jump-hop-session-1',
|
||||
themeText: draft.themeText,
|
||||
workTitle: draft.workTitle,
|
||||
workDescription: draft.workDescription,
|
||||
themeTags: draft.themeTags,
|
||||
@@ -1678,6 +1671,7 @@ function buildMockJumpHopWork(
|
||||
characterAsset,
|
||||
tileAtlasAsset,
|
||||
tileAssets,
|
||||
backButtonAsset: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5354,7 +5348,7 @@ test('match3d result trial passes generated models into first runtime mount', as
|
||||
await waitFor(() => {
|
||||
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
|
||||
'match3d-profile-draft-1',
|
||||
{},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
@@ -5447,7 +5441,7 @@ test('match3d result trial passes generated 2D image views into first runtime mo
|
||||
await waitFor(() => {
|
||||
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
|
||||
'match3d-profile-draft-2d-1',
|
||||
{},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
@@ -6471,10 +6465,10 @@ test('opening a compiled draft with a missing agent session falls back to draft
|
||||
await waitFor(() => {
|
||||
expect(fallbackDraftPanel.getAttribute('aria-hidden')).toBe('false');
|
||||
expect(
|
||||
within(fallbackDraftPanel).getByText(
|
||||
screen.getAllByText(
|
||||
'这份共创草稿已失效,已为你返回草稿列表,请重新开始创作。',
|
||||
),
|
||||
).toBeTruthy();
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
expect(window.location.search).toBe('');
|
||||
@@ -6590,6 +6584,213 @@ test('logged out public detail gates puzzle start and remix before real actions'
|
||||
expect(remixPuzzleGalleryWork).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('logged out public jump-hop detail starts runtime without requireAuth', async () => {
|
||||
const user = userEvent.setup();
|
||||
const requireAuth = vi.fn();
|
||||
const publishedJumpHopWork: JumpHopWorkProfileResponse = {
|
||||
summary: {
|
||||
runtimeKind: 'jump-hop',
|
||||
workId: 'jump-hop-work-public-1',
|
||||
profileId: 'jump-hop-profile-public-12345678',
|
||||
ownerUserId: 'user-2',
|
||||
sourceSessionId: 'jump-hop-session-public-1',
|
||||
themeText: '云上方块',
|
||||
workTitle: '云上方块跳一跳',
|
||||
workDescription: '在云层地块之间连续弹跳。',
|
||||
themeTags: ['云层', '跳跃'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'paper-toy',
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'published',
|
||||
playCount: 3,
|
||||
updatedAt: '2026-05-29T10:00:00.000Z',
|
||||
publishedAt: '2026-05-29T10:00:00.000Z',
|
||||
publishReady: true,
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
draft: {
|
||||
templateId: 'jump-hop',
|
||||
templateName: '跳一跳',
|
||||
profileId: 'jump-hop-profile-public-12345678',
|
||||
themeText: '云上方块',
|
||||
workTitle: '云上方块跳一跳',
|
||||
workDescription: '在云层地块之间连续弹跳。',
|
||||
themeTags: ['云层', '跳跃'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'paper-toy',
|
||||
defaultCharacter: {
|
||||
characterId: 'builtin-default',
|
||||
displayName: '默认角色',
|
||||
modelKind: 'builtin-three',
|
||||
bodyColor: '#df7f40',
|
||||
accentColor: '#2563eb',
|
||||
},
|
||||
characterPrompt: '',
|
||||
tilePrompt: '云上方块',
|
||||
endMoodPrompt: null,
|
||||
characterAsset: null,
|
||||
tileAtlasAsset: null,
|
||||
tileAssets: [],
|
||||
path: null,
|
||||
coverComposite: null,
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
path: {
|
||||
seed: 'jump-hop-public-seed',
|
||||
difficulty: 'standard',
|
||||
platforms: [
|
||||
{
|
||||
platformId: 'platform-0',
|
||||
tileType: 'start',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
landingRadius: 0.7,
|
||||
perfectRadius: 0.25,
|
||||
scoreValue: 1,
|
||||
},
|
||||
{
|
||||
platformId: 'platform-1',
|
||||
tileType: 'normal',
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
landingRadius: 0.7,
|
||||
perfectRadius: 0.25,
|
||||
scoreValue: 1,
|
||||
},
|
||||
{
|
||||
platformId: 'platform-2',
|
||||
tileType: 'normal',
|
||||
x: -1,
|
||||
y: 2,
|
||||
width: 1,
|
||||
height: 1,
|
||||
landingRadius: 0.7,
|
||||
perfectRadius: 0.25,
|
||||
scoreValue: 1,
|
||||
},
|
||||
],
|
||||
finishIndex: 2,
|
||||
cameraPreset: 'portrait-top-down',
|
||||
scoring: {
|
||||
chargeToDistanceRatio: 1,
|
||||
maxChargeMs: 1800,
|
||||
hitBonus: 0,
|
||||
perfectBonus: 0,
|
||||
},
|
||||
},
|
||||
defaultCharacter: {
|
||||
characterId: 'builtin-default',
|
||||
displayName: '默认角色',
|
||||
modelKind: 'builtin-three',
|
||||
bodyColor: '#df7f40',
|
||||
accentColor: '#2563eb',
|
||||
},
|
||||
characterAsset: {
|
||||
assetId: 'builtin-character',
|
||||
imageSrc: '',
|
||||
imageObjectKey: '',
|
||||
assetObjectId: '',
|
||||
generationProvider: 'builtin',
|
||||
prompt: '',
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
tileAtlasAsset: {
|
||||
assetId: 'tile-atlas-1',
|
||||
imageSrc: '/generated-jump-hop-assets/public/atlas.png',
|
||||
imageObjectKey: 'generated-jump-hop-assets/public/atlas.png',
|
||||
assetObjectId: 'asset-tile-atlas-1',
|
||||
generationProvider: 'gpt-image-2',
|
||||
prompt: '云上方块',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
tileAssets: [],
|
||||
};
|
||||
const publishedJumpHopRun: JumpHopRuntimeRunSnapshotResponse = {
|
||||
runId: 'jump-hop-run-public-1',
|
||||
profileId: publishedJumpHopWork.summary.profileId,
|
||||
ownerUserId: '',
|
||||
status: 'playing',
|
||||
currentPlatformIndex: 0,
|
||||
successfulJumpCount: 0,
|
||||
durationMs: 0,
|
||||
score: 0,
|
||||
combo: 0,
|
||||
path: publishedJumpHopWork.path,
|
||||
lastJump: null,
|
||||
startedAtMs: 1_779_999_000_000,
|
||||
finishedAtMs: null,
|
||||
};
|
||||
|
||||
window.history.replaceState(null, '', '/works/detail?work=JH-12345678');
|
||||
vi.mocked(jumpHopClient.listGallery).mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
publicWorkCode: 'JH-12345678',
|
||||
workId: publishedJumpHopWork.summary.workId,
|
||||
profileId: publishedJumpHopWork.summary.profileId,
|
||||
ownerUserId: publishedJumpHopWork.summary.ownerUserId,
|
||||
authorDisplayName: '跳跃作者',
|
||||
themeText: publishedJumpHopWork.summary.themeText,
|
||||
workTitle: publishedJumpHopWork.summary.workTitle,
|
||||
workDescription: publishedJumpHopWork.summary.workDescription,
|
||||
coverImageSrc: null,
|
||||
themeTags: publishedJumpHopWork.summary.themeTags,
|
||||
difficulty: publishedJumpHopWork.summary.difficulty,
|
||||
stylePreset: publishedJumpHopWork.summary.stylePreset,
|
||||
publicationStatus: publishedJumpHopWork.summary.publicationStatus,
|
||||
playCount: publishedJumpHopWork.summary.playCount,
|
||||
updatedAt: publishedJumpHopWork.summary.updatedAt,
|
||||
publishedAt: publishedJumpHopWork.summary.publishedAt,
|
||||
generationStatus: publishedJumpHopWork.summary.generationStatus,
|
||||
},
|
||||
],
|
||||
hasMore: false,
|
||||
nextCursor: null,
|
||||
});
|
||||
vi.mocked(jumpHopClient.getWorkDetail).mockResolvedValue({
|
||||
item: publishedJumpHopWork,
|
||||
});
|
||||
vi.mocked(jumpHopClient.startRun).mockResolvedValue({
|
||||
run: publishedJumpHopRun,
|
||||
});
|
||||
vi.mocked(jumpHopClient.getLeaderboard).mockResolvedValue({
|
||||
profileId: publishedJumpHopWork.summary.profileId,
|
||||
items: [],
|
||||
viewerBest: null,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
authValue={createAuthValue({
|
||||
user: null,
|
||||
canAccessProtectedData: false,
|
||||
openLoginModal: () => {},
|
||||
requireAuth,
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('详情')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '启动' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(jumpHopClient.startRun).toHaveBeenCalledWith(
|
||||
publishedJumpHopWork.summary.profileId,
|
||||
expect.objectContaining({
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
runtimeMode: 'published',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(requireAuth).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('owned public puzzle detail edits original draft instead of remixing', async () => {
|
||||
const user = userEvent.setup();
|
||||
const ownedPuzzleWork = {
|
||||
@@ -8050,6 +8251,7 @@ test('direct jump hop result route restores work detail by profile id', async ()
|
||||
profileId: 'jump-hop-profile-restore-1',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: null,
|
||||
themeText: '恢复后的云端跳台',
|
||||
workTitle: '恢复后的云端跳台',
|
||||
workDescription: '从 profileId 回读完整跳一跳结果。',
|
||||
themeTags: ['云朵'],
|
||||
@@ -9222,7 +9424,7 @@ test('public code search opens a published Match3D work by M3 code and starts ru
|
||||
await waitFor(() => {
|
||||
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
|
||||
'match3d-profile-public-1',
|
||||
{},
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
@@ -9294,7 +9496,7 @@ test('published Match3D runtime receives persisted generated models', async () =
|
||||
await waitFor(() => {
|
||||
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
|
||||
'match3d-profile-model-1',
|
||||
{},
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
|
||||
@@ -111,53 +111,11 @@ const FALLBACK_UNIFIED_CREATION_SPECS: Record<
|
||||
resultStage: 'jump-hop-result',
|
||||
fields: [
|
||||
{
|
||||
id: 'workTitle',
|
||||
id: 'themeText',
|
||||
kind: 'text',
|
||||
label: '作品标题',
|
||||
label: '主题',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'workDescription',
|
||||
kind: 'text',
|
||||
label: '作品简介',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'themeTags',
|
||||
kind: 'text',
|
||||
label: '主题标签',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'difficulty',
|
||||
kind: 'select',
|
||||
label: '难度',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'stylePreset',
|
||||
kind: 'select',
|
||||
label: '风格',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'characterPrompt',
|
||||
kind: 'text',
|
||||
label: '角色提示词',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'tilePrompt',
|
||||
kind: 'text',
|
||||
label: '地块提示词',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'endMoodPrompt',
|
||||
kind: 'text',
|
||||
label: '终点氛围',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
'wooden-fish': {
|
||||
|
||||
@@ -33,7 +33,7 @@ function createSessionResponse(): JumpHopSessionResponse {
|
||||
};
|
||||
}
|
||||
|
||||
test('jump hop workspace submits structured payload after required fields are filled', async () => {
|
||||
test('jump hop workspace submits theme payload after required field is filled', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmitted = vi.fn();
|
||||
const sessionResponse = createSessionResponse();
|
||||
@@ -46,14 +46,11 @@ test('jump hop workspace submits structured payload after required fields are fi
|
||||
const submitButton = screen.getByRole('button', { name: '生成' });
|
||||
expect(submitButton).toHaveProperty('disabled', true);
|
||||
|
||||
await user.type(screen.getByLabelText('作品标题'), '云朵跳台');
|
||||
await user.type(screen.getByLabelText('作品简介'), '在云端一路跳到星星。');
|
||||
await user.type(screen.getByLabelText('主题标签'), '云朵 星星');
|
||||
await user.selectOptions(screen.getByLabelText('难度'), 'standard');
|
||||
await user.selectOptions(screen.getByLabelText('风格'), 'paper-toy');
|
||||
await user.type(screen.getByLabelText('角色提示词'), '一只纸片小兔');
|
||||
await user.type(screen.getByLabelText('地块提示词'), '柔软云朵平台');
|
||||
await user.type(screen.getByLabelText('终点氛围'), '星光门');
|
||||
expect(screen.getByLabelText('主题')).toBeTruthy();
|
||||
expect(screen.queryByLabelText('作品标题')).toBeNull();
|
||||
expect(screen.queryByLabelText('角色提示词')).toBeNull();
|
||||
|
||||
await user.type(screen.getByLabelText('主题'), '云朵跳台');
|
||||
|
||||
expect(submitButton).toHaveProperty('disabled', false);
|
||||
await user.click(submitButton);
|
||||
@@ -61,21 +58,22 @@ test('jump hop workspace submits structured payload after required fields are fi
|
||||
await waitFor(() => {
|
||||
expect(mockCreateSession).toHaveBeenCalledWith({
|
||||
templateId: 'jump-hop',
|
||||
workTitle: '云朵跳台',
|
||||
workDescription: '在云端一路跳到星星。',
|
||||
themeTags: ['云朵', '星星'],
|
||||
themeText: '云朵跳台',
|
||||
workTitle: '云朵跳台跳一跳',
|
||||
workDescription: '云朵跳台主题的俯视角跳跃作品',
|
||||
themeTags: ['云朵跳台', '跳一跳', '休闲'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'paper-toy',
|
||||
characterPrompt: '一只纸片小兔',
|
||||
tilePrompt: '柔软云朵平台',
|
||||
endMoodPrompt: '星光门',
|
||||
stylePreset: 'minimal-blocks',
|
||||
characterPrompt: '内置默认 3D 角色',
|
||||
tilePrompt: '云朵跳台主题的正面30度视角主题物体图集,物体本身作为跳跃落点',
|
||||
endMoodPrompt: null,
|
||||
});
|
||||
});
|
||||
expect(onSubmitted).toHaveBeenCalledWith(
|
||||
sessionResponse,
|
||||
expect.objectContaining({
|
||||
templateId: 'jump-hop',
|
||||
workTitle: '云朵跳台',
|
||||
themeText: '云朵跳台',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,9 +2,7 @@ import { ArrowLeft, Loader2, Send } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import type {
|
||||
JumpHopDifficulty,
|
||||
JumpHopSessionResponse,
|
||||
JumpHopStylePreset,
|
||||
JumpHopWorkspaceCreateRequest,
|
||||
} from '../../../../packages/shared/src/contracts/jumpHop';
|
||||
import { jumpHopClient } from '../../../services/jump-hop/jumpHopClient';
|
||||
@@ -22,27 +20,31 @@ type JumpHopCreationWorkspaceProps = {
|
||||
};
|
||||
|
||||
type JumpHopWorkspaceFormState = {
|
||||
workTitle: string;
|
||||
workDescription: string;
|
||||
themeTags: string;
|
||||
difficulty: JumpHopDifficulty;
|
||||
stylePreset: JumpHopStylePreset;
|
||||
characterPrompt: string;
|
||||
tilePrompt: string;
|
||||
endMoodPrompt: string;
|
||||
themeText: string;
|
||||
};
|
||||
|
||||
const DEFAULT_FORM_STATE: JumpHopWorkspaceFormState = {
|
||||
workTitle: '',
|
||||
workDescription: '',
|
||||
themeTags: '',
|
||||
difficulty: 'easy',
|
||||
stylePreset: 'minimal-blocks',
|
||||
characterPrompt: '',
|
||||
tilePrompt: '',
|
||||
endMoodPrompt: '',
|
||||
themeText: '',
|
||||
};
|
||||
|
||||
function buildJumpHopWorkspacePayload(
|
||||
formState: JumpHopWorkspaceFormState,
|
||||
): JumpHopWorkspaceCreateRequest {
|
||||
const themeText = formState.themeText.trim();
|
||||
return {
|
||||
templateId: 'jump-hop',
|
||||
themeText,
|
||||
workTitle: `${themeText}跳一跳`,
|
||||
workDescription: `${themeText}主题的俯视角跳跃作品`,
|
||||
themeTags: [themeText, '跳一跳', '休闲'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'minimal-blocks',
|
||||
characterPrompt: '内置默认 3D 角色',
|
||||
tilePrompt: `${themeText}主题的正面30度视角主题物体图集,物体本身作为跳跃落点`,
|
||||
endMoodPrompt: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function JumpHopCreationWorkspace({
|
||||
isBusy = false,
|
||||
error = null,
|
||||
@@ -56,14 +58,7 @@ export function JumpHopCreationWorkspace({
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const canSubmit = useMemo(
|
||||
() =>
|
||||
Boolean(
|
||||
formState.workTitle.trim() &&
|
||||
formState.workDescription.trim() &&
|
||||
formState.themeTags.trim() &&
|
||||
formState.characterPrompt.trim() &&
|
||||
formState.tilePrompt.trim(),
|
||||
),
|
||||
() => Boolean(formState.themeText.trim()),
|
||||
[formState],
|
||||
);
|
||||
|
||||
@@ -77,20 +72,7 @@ export function JumpHopCreationWorkspace({
|
||||
setLocalError(null);
|
||||
|
||||
try {
|
||||
const payload: JumpHopWorkspaceCreateRequest = {
|
||||
templateId: 'jump-hop',
|
||||
workTitle: formState.workTitle.trim(),
|
||||
workDescription: formState.workDescription.trim(),
|
||||
themeTags: formState.themeTags
|
||||
.split(/[,,、\s]+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean),
|
||||
difficulty: formState.difficulty,
|
||||
stylePreset: formState.stylePreset,
|
||||
characterPrompt: formState.characterPrompt.trim(),
|
||||
tilePrompt: formState.tilePrompt.trim(),
|
||||
endMoodPrompt: formState.endMoodPrompt.trim() || null,
|
||||
};
|
||||
const payload = buildJumpHopWorkspacePayload(formState);
|
||||
const response = await jumpHopClient.createSession(payload);
|
||||
onSubmitted(response, payload);
|
||||
} catch (caughtError) {
|
||||
@@ -124,143 +106,22 @@ export function JumpHopCreationWorkspace({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="grid gap-3">
|
||||
<label className="block sm:col-span-2">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
作品标题
|
||||
主题
|
||||
</span>
|
||||
<input
|
||||
value={formState.workTitle}
|
||||
value={formState.themeText}
|
||||
onChange={(event) =>
|
||||
setFormState((current) => ({
|
||||
...current,
|
||||
workTitle: event.target.value,
|
||||
themeText: event.target.value,
|
||||
}))
|
||||
}
|
||||
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-3 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
/>
|
||||
</label>
|
||||
<label className="block sm:col-span-2">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
作品简介
|
||||
</span>
|
||||
<textarea
|
||||
value={formState.workDescription}
|
||||
onChange={(event) =>
|
||||
setFormState((current) => ({
|
||||
...current,
|
||||
workDescription: event.target.value,
|
||||
}))
|
||||
}
|
||||
rows={3}
|
||||
className="mt-2 w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-3 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
|
||||
/>
|
||||
</label>
|
||||
<label className="block sm:col-span-2">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
主题标签
|
||||
</span>
|
||||
<input
|
||||
value={formState.themeTags}
|
||||
onChange={(event) =>
|
||||
setFormState((current) => ({
|
||||
...current,
|
||||
themeTags: event.target.value,
|
||||
}))
|
||||
}
|
||||
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-3 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
难度
|
||||
</span>
|
||||
<select
|
||||
value={formState.difficulty}
|
||||
onChange={(event) =>
|
||||
setFormState((current) => ({
|
||||
...current,
|
||||
difficulty: event.target.value as JumpHopDifficulty,
|
||||
}))
|
||||
}
|
||||
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-3 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
>
|
||||
<option value="easy">easy</option>
|
||||
<option value="standard">standard</option>
|
||||
<option value="advanced">advanced</option>
|
||||
<option value="challenge">challenge</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
风格
|
||||
</span>
|
||||
<select
|
||||
value={formState.stylePreset}
|
||||
onChange={(event) =>
|
||||
setFormState((current) => ({
|
||||
...current,
|
||||
stylePreset: event.target.value as JumpHopStylePreset,
|
||||
}))
|
||||
}
|
||||
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-3 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
>
|
||||
<option value="minimal-blocks">minimal-blocks</option>
|
||||
<option value="paper-toy">paper-toy</option>
|
||||
<option value="neon-glass">neon-glass</option>
|
||||
<option value="forest-stone">forest-stone</option>
|
||||
<option value="future-metal">future-metal</option>
|
||||
<option value="custom">custom</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="block sm:col-span-2">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
角色提示词
|
||||
</span>
|
||||
<textarea
|
||||
value={formState.characterPrompt}
|
||||
onChange={(event) =>
|
||||
setFormState((current) => ({
|
||||
...current,
|
||||
characterPrompt: event.target.value,
|
||||
}))
|
||||
}
|
||||
rows={3}
|
||||
className="mt-2 w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-3 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
|
||||
/>
|
||||
</label>
|
||||
<label className="block sm:col-span-2">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
地块提示词
|
||||
</span>
|
||||
<textarea
|
||||
value={formState.tilePrompt}
|
||||
onChange={(event) =>
|
||||
setFormState((current) => ({
|
||||
...current,
|
||||
tilePrompt: event.target.value,
|
||||
}))
|
||||
}
|
||||
rows={3}
|
||||
className="mt-2 w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-3 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
|
||||
/>
|
||||
</label>
|
||||
<label className="block sm:col-span-2">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
终点氛围
|
||||
</span>
|
||||
<textarea
|
||||
value={formState.endMoodPrompt}
|
||||
onChange={(event) =>
|
||||
setFormState((current) => ({
|
||||
...current,
|
||||
endMoodPrompt: event.target.value,
|
||||
}))
|
||||
}
|
||||
rows={2}
|
||||
className="mt-2 w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-3 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{localError || error ? (
|
||||
|
||||
Reference in New Issue
Block a user