1
This commit is contained in:
@@ -104,11 +104,12 @@ function createSession(
|
||||
stage: 'ready_to_publish',
|
||||
anchorPack,
|
||||
draft: {
|
||||
workTitle: '暖灯猫街作品',
|
||||
workDescription: '一套雨夜猫街主题拼图。',
|
||||
workTitle: overrides.draft?.workTitle ?? '暖灯猫街作品',
|
||||
workDescription:
|
||||
overrides.draft?.workDescription ?? '一套雨夜猫街主题拼图。',
|
||||
levelName: level.levelName,
|
||||
summary: level.pictureDescription,
|
||||
themeTags: ['猫咪', '雨夜', '暖灯'],
|
||||
themeTags: overrides.draft?.themeTags ?? ['猫咪', '雨夜', '暖灯'],
|
||||
forbiddenDirectives: [],
|
||||
creatorIntent: null,
|
||||
anchorPack,
|
||||
@@ -119,6 +120,7 @@ function createSession(
|
||||
generationStatus: 'ready',
|
||||
levels: [level],
|
||||
metadata: null,
|
||||
...overrides.draft,
|
||||
},
|
||||
messages: [],
|
||||
lastAssistantReply: null,
|
||||
@@ -199,7 +201,7 @@ describe('PuzzleResultView', () => {
|
||||
workTitle: '暖灯猫街合集',
|
||||
workDescription: '一套雨夜猫街主题拼图。',
|
||||
levelName: '雨夜猫街',
|
||||
summary: '屋檐下的猫与暖灯街角。',
|
||||
summary: '一套雨夜猫街主题拼图。',
|
||||
themeTags: ['猫咪', '雨夜', '暖灯'],
|
||||
levels: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -250,7 +252,7 @@ describe('PuzzleResultView', () => {
|
||||
candidateCount: 1,
|
||||
workTitle: '暖灯猫街作品',
|
||||
workDescription: '一套雨夜猫街主题拼图。',
|
||||
summary: '一只猫在雨夜灯牌下回头。',
|
||||
summary: '一套雨夜猫街主题拼图。',
|
||||
themeTags: ['猫咪', '雨夜', '暖灯'],
|
||||
levelsJson: expect.any(String),
|
||||
});
|
||||
@@ -280,7 +282,7 @@ describe('PuzzleResultView', () => {
|
||||
expect(onStartTestRun).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
levelName: '暖灯猫街',
|
||||
summary: '一只猫在雨夜灯牌下回头。',
|
||||
summary: '一套雨夜猫街主题拼图。',
|
||||
levels: [
|
||||
expect.objectContaining({
|
||||
levelId: 'puzzle-level-1',
|
||||
@@ -386,7 +388,7 @@ describe('PuzzleResultView', () => {
|
||||
candidateCount: 1,
|
||||
workTitle: '暖灯猫街作品',
|
||||
workDescription: '一套雨夜猫街主题拼图。',
|
||||
summary: '新关卡里有一座发光钟楼。',
|
||||
summary: '一套雨夜猫街主题拼图。',
|
||||
themeTags: ['猫咪', '雨夜', '暖灯'],
|
||||
levelsJson: expect.any(String),
|
||||
});
|
||||
@@ -427,7 +429,7 @@ describe('PuzzleResultView', () => {
|
||||
workTitle: '暖灯猫街作品',
|
||||
workDescription: '一套雨夜猫街主题拼图。',
|
||||
levelName: '雨夜猫街',
|
||||
summary: '屋檐下的猫与暖灯街角。',
|
||||
summary: '一套雨夜猫街主题拼图。',
|
||||
themeTags: ['猫咪', '雨夜', '暖灯'],
|
||||
}),
|
||||
);
|
||||
@@ -440,6 +442,57 @@ describe('PuzzleResultView', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('generates six tags after work title and description are filled', () => {
|
||||
const onExecuteAction = vi.fn();
|
||||
|
||||
render(
|
||||
<PuzzleResultView
|
||||
session={createSession({
|
||||
draft: {
|
||||
...createSession().draft!,
|
||||
workTitle: '雨夜猫街',
|
||||
workDescription: '',
|
||||
themeTags: [],
|
||||
},
|
||||
resultPreview: {
|
||||
draft: createSession().draft!,
|
||||
publishReady: false,
|
||||
blockers: [
|
||||
{
|
||||
id: 'invalid-tag-count',
|
||||
code: 'INVALID_TAG_COUNT',
|
||||
message: '正式标签数量必须在 3 到 6 之间',
|
||||
},
|
||||
],
|
||||
qualityFindings: [],
|
||||
},
|
||||
})}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={onExecuteAction}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '作品信息' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'AI生成作品标签' }));
|
||||
expect(screen.getByText('请先填写作品名称和作品描述。')).toBeTruthy();
|
||||
expect(onExecuteAction).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('作品描述'), {
|
||||
target: { value: '一套雨夜猫街主题拼图。' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: 'AI生成作品标签' }));
|
||||
|
||||
expect(onExecuteAction).toHaveBeenCalledWith({
|
||||
action: 'generate_puzzle_tags',
|
||||
workTitle: '雨夜猫街',
|
||||
workDescription: '一套雨夜猫街主题拼图。',
|
||||
levelName: '雨夜猫街',
|
||||
summary: '一套雨夜猫街主题拼图。',
|
||||
themeTags: [],
|
||||
levelsJson: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
test('selects a history puzzle asset as reference image for the selected level', async () => {
|
||||
const onExecuteAction = vi.fn();
|
||||
vi.mocked(puzzleAssetClient.listHistoryAssets).mockResolvedValue([
|
||||
@@ -496,7 +549,7 @@ describe('PuzzleResultView', () => {
|
||||
candidateCount: 1,
|
||||
workTitle: '暖灯猫街作品',
|
||||
workDescription: '一套雨夜猫街主题拼图。',
|
||||
summary: '屋檐下的猫与暖灯街角。',
|
||||
summary: '一套雨夜猫街主题拼图。',
|
||||
themeTags: ['猫咪', '雨夜', '暖灯'],
|
||||
levelsJson: expect.any(String),
|
||||
});
|
||||
|
||||
@@ -129,7 +129,7 @@ function syncDraftFromEditState(
|
||||
const primaryLevel = levels[0] ?? buildFallbackLevelFromDraft(draft);
|
||||
return {
|
||||
...draft,
|
||||
workTitle: editState.workTitle.trim() || draft.workTitle,
|
||||
workTitle: editState.workTitle.trim(),
|
||||
workDescription: editState.workDescription.trim(),
|
||||
levelName: primaryLevel.levelName,
|
||||
summary: editState.workDescription.trim(),
|
||||
@@ -145,8 +145,8 @@ function syncDraftFromEditState(
|
||||
|
||||
function createDraftEditState(draft: PuzzleResultDraft): DraftEditState {
|
||||
return {
|
||||
workTitle: draft.workTitle || draft.levelName,
|
||||
workDescription: draft.workDescription || '',
|
||||
workTitle: draft.workTitle ?? '',
|
||||
workDescription: draft.workDescription ?? '',
|
||||
themeTags: normalizeThemeTagInput(draft.themeTags.join(',')),
|
||||
levels: normalizeDraftLevels(draft),
|
||||
};
|
||||
@@ -219,16 +219,7 @@ function buildPublishReady(
|
||||
|
||||
return {
|
||||
blockers: [...new Set(blockers.filter(Boolean))],
|
||||
publishReady:
|
||||
Boolean(session.resultPreview?.publishReady) &&
|
||||
Boolean(editState.workTitle.trim()) &&
|
||||
Boolean(editState.workDescription.trim()) &&
|
||||
editState.themeTags.length >= PUZZLE_MIN_THEME_TAG_COUNT &&
|
||||
editState.themeTags.length <= PUZZLE_MAX_THEME_TAG_COUNT &&
|
||||
levels.length > 0 &&
|
||||
levels.every(
|
||||
(level) => level.levelName.trim() && resolveLevelFormalImageSrc(level),
|
||||
),
|
||||
publishReady: blockers.filter(Boolean).length === 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -308,11 +299,15 @@ function PuzzleResultTabs({
|
||||
function PuzzleThemeTagEditor({
|
||||
editState,
|
||||
isBusy,
|
||||
error,
|
||||
onChange,
|
||||
onGenerateTags,
|
||||
}: {
|
||||
editState: DraftEditState;
|
||||
isBusy: boolean;
|
||||
error: string | null;
|
||||
onChange: (nextState: DraftEditState) => void;
|
||||
onGenerateTags: () => void;
|
||||
}) {
|
||||
const [newTagText, setNewTagText] = useState('');
|
||||
const [isAddingTag, setIsAddingTag] = useState(false);
|
||||
@@ -339,18 +334,34 @@ function PuzzleThemeTagEditor({
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
作品标签
|
||||
</div>
|
||||
{!isAddingTag ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => setIsAddingTag(true)}
|
||||
onClick={onGenerateTags}
|
||||
className="platform-icon-button h-9 w-9"
|
||||
aria-label="新增作品标签"
|
||||
title="新增作品标签"
|
||||
aria-label="AI生成作品标签"
|
||||
title="AI生成作品标签"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{isBusy ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
{!isAddingTag ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => setIsAddingTag(true)}
|
||||
className="platform-icon-button h-9 w-9"
|
||||
aria-label="新增作品标签"
|
||||
title="新增作品标签"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
@@ -430,6 +441,11 @@ function PuzzleThemeTagEditor({
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{error ? (
|
||||
<div className="platform-banner platform-banner--danger mt-3 rounded-2xl text-sm leading-6">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1191,12 +1207,16 @@ function PuzzleLevelListTab({
|
||||
|
||||
function PuzzleWorkInfoTab({
|
||||
editState,
|
||||
tagGenerationError,
|
||||
isBusy,
|
||||
onChange,
|
||||
onGenerateTags,
|
||||
}: {
|
||||
editState: DraftEditState;
|
||||
tagGenerationError: string | null;
|
||||
isBusy: boolean;
|
||||
onChange: (nextState: DraftEditState) => void;
|
||||
onGenerateTags: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
@@ -1233,8 +1253,10 @@ function PuzzleWorkInfoTab({
|
||||
|
||||
<PuzzleThemeTagEditor
|
||||
editState={editState}
|
||||
error={tagGenerationError}
|
||||
isBusy={isBusy}
|
||||
onChange={onChange}
|
||||
onGenerateTags={onGenerateTags}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -1304,6 +1326,9 @@ export function PuzzleResultView({
|
||||
const [autoSaveState, setAutoSaveState] =
|
||||
useState<PuzzleAutoSaveState>('idle');
|
||||
const [autoSaveError, setAutoSaveError] = useState<string | null>(null);
|
||||
const [tagGenerationError, setTagGenerationError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const savedEditStateRef = useRef<DraftEditState | null>(
|
||||
draft ? createDraftEditState(draft) : null,
|
||||
);
|
||||
@@ -1314,6 +1339,7 @@ export function PuzzleResultView({
|
||||
setActiveLevelId(null);
|
||||
setAutoSaveState('idle');
|
||||
setAutoSaveError(null);
|
||||
setTagGenerationError(null);
|
||||
return;
|
||||
}
|
||||
const nextState = createDraftEditState(draft);
|
||||
@@ -1327,6 +1353,7 @@ export function PuzzleResultView({
|
||||
);
|
||||
setAutoSaveState('idle');
|
||||
setAutoSaveError(null);
|
||||
setTagGenerationError(null);
|
||||
}, [draft]);
|
||||
|
||||
const syncedDraft = useMemo(() => {
|
||||
@@ -1445,7 +1472,7 @@ export function PuzzleResultView({
|
||||
const buildLevelDraft = (level: PuzzleDraftLevel): PuzzleResultDraft => ({
|
||||
...syncedDraft,
|
||||
levelName: level.levelName,
|
||||
summary: level.pictureDescription,
|
||||
summary: editState.workDescription.trim(),
|
||||
candidates: level.candidates,
|
||||
selectedCandidateId: level.selectedCandidateId,
|
||||
coverImageSrc: resolveLevelFormalImageSrc(level) || level.coverImageSrc,
|
||||
@@ -1498,8 +1525,28 @@ export function PuzzleResultView({
|
||||
) : (
|
||||
<PuzzleWorkInfoTab
|
||||
editState={editState}
|
||||
tagGenerationError={tagGenerationError}
|
||||
isBusy={isBusy}
|
||||
onChange={setEditState}
|
||||
onGenerateTags={() => {
|
||||
const workTitle = editState.workTitle.trim();
|
||||
const workDescription = editState.workDescription.trim();
|
||||
if (!workTitle || !workDescription) {
|
||||
setTagGenerationError('请先填写作品名称和作品描述。');
|
||||
return;
|
||||
}
|
||||
setTagGenerationError(null);
|
||||
const firstLevel = editState.levels[0] ?? null;
|
||||
onExecuteAction({
|
||||
action: 'generate_puzzle_tags',
|
||||
workTitle,
|
||||
workDescription,
|
||||
levelName: firstLevel?.levelName.trim(),
|
||||
summary: workDescription,
|
||||
themeTags: editState.themeTags,
|
||||
levelsJson: JSON.stringify(editState.levels),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user