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:
@@ -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