1
This commit is contained in:
@@ -16,7 +16,6 @@ import { createPortal } from 'react-dom';
|
||||
import type { PuzzleAgentActionRequest } from '../../../packages/shared/src/contracts/puzzleAgentActions';
|
||||
import type {
|
||||
PuzzleDraftLevel,
|
||||
PuzzleGeneratedImageCandidate,
|
||||
PuzzleResultDraft,
|
||||
} from '../../../packages/shared/src/contracts/puzzleAgentDraft';
|
||||
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||
@@ -84,7 +83,7 @@ function resolveLevelFormalImageSrc(level: PuzzleDraftLevel) {
|
||||
function buildFallbackLevelFromDraft(draft: PuzzleResultDraft): PuzzleDraftLevel {
|
||||
return {
|
||||
levelId: 'puzzle-level-1',
|
||||
levelName: draft.levelName || draft.workTitle || '第一关',
|
||||
levelName: draft.levelName || '',
|
||||
pictureDescription: draft.summary,
|
||||
candidates: draft.candidates,
|
||||
selectedCandidateId: draft.selectedCandidateId,
|
||||
@@ -103,7 +102,7 @@ function normalizeDraftLevels(draft: PuzzleResultDraft) {
|
||||
return sourceLevels.map((level, index) => ({
|
||||
...level,
|
||||
levelId: level.levelId?.trim() || `puzzle-level-${index + 1}`,
|
||||
levelName: level.levelName?.trim() || `第${index + 1}关`,
|
||||
levelName: level.levelName?.trim() || '',
|
||||
pictureDescription: level.pictureDescription?.trim() || draft.summary,
|
||||
candidates: level.candidates ?? [],
|
||||
selectedCandidateId: level.selectedCandidateId ?? null,
|
||||
@@ -148,7 +147,7 @@ function createBlankPuzzleLevel(existingLevels: PuzzleDraftLevel[]): PuzzleDraft
|
||||
const nextIndex = existingLevels.length + 1;
|
||||
return {
|
||||
levelId: `puzzle-level-${Date.now()}-${nextIndex}`,
|
||||
levelName: `第${nextIndex}关`,
|
||||
levelName: '',
|
||||
pictureDescription: '',
|
||||
candidates: [],
|
||||
selectedCandidateId: null,
|
||||
@@ -585,6 +584,7 @@ function PuzzleLevelDetailDialog({
|
||||
const [referenceImageError, setReferenceImageError] = useState<string | null>(null);
|
||||
const [isHistoryPickerOpen, setIsHistoryPickerOpen] = useState(false);
|
||||
const formalImageSrc = resolveLevelFormalImageSrc(level);
|
||||
const hasFormalImage = Boolean(formalImageSrc);
|
||||
|
||||
const handleReferenceImageChange = async (
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
@@ -626,7 +626,7 @@ function PuzzleLevelDetailDialog({
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="关卡详情"
|
||||
className="platform-modal-shell platform-remap-surface flex max-h-[min(94vh,50rem)] w-full max-w-5xl flex-col overflow-hidden rounded-t-[1.75rem] shadow-[0_24px_80px_rgba(0,0,0,0.55)] sm:rounded-[1.75rem]"
|
||||
className="platform-modal-shell platform-remap-surface flex max-h-[min(94vh,50rem)] w-full max-w-2xl flex-col overflow-hidden rounded-t-[1.75rem] shadow-[0_24px_80px_rgba(0,0,0,0.55)] sm:rounded-[1.75rem]"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
|
||||
@@ -644,167 +644,159 @@ function PuzzleLevelDetailDialog({
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
||||
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_20rem]">
|
||||
<div className="space-y-4">
|
||||
<section className="platform-subpanel rounded-[1.35rem] p-4">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
关卡名称
|
||||
</div>
|
||||
<input
|
||||
value={level.levelName}
|
||||
disabled={isBusy}
|
||||
onChange={(event) =>
|
||||
onLevelChange({ ...level, levelName: event.target.value })
|
||||
}
|
||||
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
aria-label="关卡名称"
|
||||
/>
|
||||
</section>
|
||||
<div className="space-y-4">
|
||||
<section className="platform-subpanel rounded-[1.35rem] p-4">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
关卡名称
|
||||
</div>
|
||||
<input
|
||||
value={level.levelName}
|
||||
disabled={isBusy}
|
||||
onChange={(event) =>
|
||||
onLevelChange({ ...level, levelName: event.target.value })
|
||||
}
|
||||
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
aria-label="关卡名称"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="platform-subpanel rounded-[1.35rem] p-4">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
画面描述
|
||||
</div>
|
||||
<div className="relative mt-3">
|
||||
<textarea
|
||||
value={level.pictureDescription}
|
||||
disabled={isBusy}
|
||||
rows={9}
|
||||
onChange={(event) =>
|
||||
onLevelChange({
|
||||
...level,
|
||||
pictureDescription: event.target.value,
|
||||
})
|
||||
}
|
||||
className="w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 pb-16 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
|
||||
aria-label="画面描述"
|
||||
/>
|
||||
<div className="absolute bottom-3 right-3 flex items-center gap-2">
|
||||
<label
|
||||
className={`inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border border-amber-300/70 bg-white/96 text-amber-700 shadow-sm transition hover:bg-amber-50 ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
title={referenceImageSrc ? '更换参考图' : '添加参考图'}
|
||||
>
|
||||
<ImagePlus className="h-4 w-4" />
|
||||
<span className="sr-only">
|
||||
{referenceImageSrc ? '更换参考图' : '添加参考图'}
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp"
|
||||
disabled={isBusy}
|
||||
onChange={(event) => {
|
||||
void handleReferenceImageChange(event);
|
||||
}}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => setIsHistoryPickerOpen(true)}
|
||||
className={`inline-flex h-10 w-10 items-center justify-center rounded-full border border-[var(--platform-subpanel-border)] bg-white/96 text-[var(--platform-text-strong)] shadow-sm transition hover:bg-[var(--platform-subpanel-fill)] ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
aria-label="从历史拼图素材库选择"
|
||||
title="从历史拼图素材库选择"
|
||||
>
|
||||
<Images className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{referenceImageSrc ? (
|
||||
<div className="mt-3 flex items-center gap-3 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-3 py-3">
|
||||
<div className="h-14 w-14 overflow-hidden rounded-[0.9rem] bg-[var(--platform-subpanel-fill)]">
|
||||
<img
|
||||
src={referenceImageSrc}
|
||||
alt="拼图参考图"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-semibold text-[var(--platform-text-strong)]">
|
||||
{referenceImageLabel || '已选择参考图'}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
setReferenceImageSrc('');
|
||||
setReferenceImageLabel('');
|
||||
setReferenceImageError(null);
|
||||
}}
|
||||
className="platform-icon-button h-9 w-9"
|
||||
aria-label="移除参考图"
|
||||
title="移除参考图"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{referenceImageError ? (
|
||||
<div className="mt-2 text-xs leading-5 text-red-600">
|
||||
{referenceImageError}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{hasFormalImage ? (
|
||||
<section className="platform-subpanel rounded-[1.35rem] p-4">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
画面图
|
||||
</div>
|
||||
<div className="mt-3 aspect-square overflow-hidden rounded-[1.15rem] bg-[var(--platform-subpanel-fill)]">
|
||||
{formalImageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={formalImageSrc}
|
||||
refreshKey={`${imageRefreshKey}:${level.levelId}`}
|
||||
alt={level.levelName || draft.workTitle || '拼图关卡'}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-sm text-[var(--platform-text-soft)]">
|
||||
暂无正式图
|
||||
</div>
|
||||
)}
|
||||
<div className="relative mt-3 aspect-square overflow-hidden rounded-[1.15rem] bg-[var(--platform-subpanel-fill)]">
|
||||
<ResolvedAssetImage
|
||||
src={formalImageSrc}
|
||||
refreshKey={`${imageRefreshKey}:${level.levelId}`}
|
||||
alt={level.levelName || draft.workTitle || '拼图关卡'}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => setIsHistoryPickerOpen(true)}
|
||||
className={`absolute bottom-3 right-3 inline-flex h-10 w-10 items-center justify-center rounded-full border border-[var(--platform-subpanel-border)] bg-white/96 text-[var(--platform-text-strong)] shadow-sm transition hover:bg-[var(--platform-subpanel-fill)] ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
aria-label="从历史拼图素材库选择"
|
||||
title="从历史拼图素材库选择"
|
||||
>
|
||||
<Images className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
onGenerate(
|
||||
level.levelId,
|
||||
level.pictureDescription.trim() || undefined,
|
||||
referenceImageSrc || undefined,
|
||||
);
|
||||
}}
|
||||
className="inline-flex w-full items-center justify-center gap-2 rounded-full bg-amber-600 px-4 py-3 text-sm font-bold text-white disabled:opacity-45"
|
||||
>
|
||||
{isBusy ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||
<Sparkles className="h-4 w-4" />
|
||||
重新生成画面
|
||||
</button>
|
||||
|
||||
{onStartTestRun ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy || !formalImageSrc}
|
||||
onClick={() => onStartTestRun(level)}
|
||||
className={`platform-button platform-button--secondary w-full ${isBusy || !formalImageSrc ? 'opacity-55' : ''}`}
|
||||
<section className="platform-subpanel rounded-[1.35rem] p-4">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
画面描述
|
||||
</div>
|
||||
<div className="relative mt-3">
|
||||
<textarea
|
||||
value={level.pictureDescription}
|
||||
disabled={isBusy}
|
||||
rows={9}
|
||||
onChange={(event) =>
|
||||
onLevelChange({
|
||||
...level,
|
||||
pictureDescription: event.target.value,
|
||||
})
|
||||
}
|
||||
className="w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 pb-16 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
|
||||
aria-label="画面描述"
|
||||
/>
|
||||
<label
|
||||
className={`absolute bottom-3 right-3 inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border border-amber-300/70 bg-white/96 text-amber-700 shadow-sm transition hover:bg-amber-50 ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
title={referenceImageSrc ? '更换参考图' : '添加参考图'}
|
||||
>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Play className="h-4 w-4" />
|
||||
体验该关
|
||||
<ImagePlus className="h-4 w-4" />
|
||||
<span className="sr-only">
|
||||
{referenceImageSrc ? '更换参考图' : '添加参考图'}
|
||||
</span>
|
||||
</button>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp"
|
||||
disabled={isBusy}
|
||||
onChange={(event) => {
|
||||
void handleReferenceImageChange(event);
|
||||
}}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{referenceImageSrc ? (
|
||||
<div className="mt-3 flex items-center gap-3 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-3 py-3">
|
||||
<div className="h-14 w-14 overflow-hidden rounded-[0.9rem] bg-[var(--platform-subpanel-fill)]">
|
||||
<img
|
||||
src={referenceImageSrc}
|
||||
alt="拼图参考图"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-semibold text-[var(--platform-text-strong)]">
|
||||
{referenceImageLabel || '已选择参考图'}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
setReferenceImageSrc('');
|
||||
setReferenceImageLabel('');
|
||||
setReferenceImageError(null);
|
||||
}}
|
||||
className="platform-icon-button h-9 w-9"
|
||||
aria-label="移除参考图"
|
||||
title="移除参考图"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{referenceImageError ? (
|
||||
<div className="mt-2 text-xs leading-5 text-red-600">
|
||||
{referenceImageError}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 space-y-3 border-t border-[var(--platform-subpanel-border)] bg-[var(--platform-page-fill)] px-5 py-4 pb-[calc(env(safe-area-inset-bottom,0px)+1rem)]">
|
||||
{onStartTestRun && hasFormalImage ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => onStartTestRun(level)}
|
||||
className={`platform-button platform-button--secondary w-full ${isBusy ? 'opacity-55' : ''}`}
|
||||
>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Play className="h-4 w-4" />
|
||||
关卡测试
|
||||
</span>
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
onGenerate(
|
||||
level.levelId,
|
||||
level.pictureDescription.trim() || undefined,
|
||||
referenceImageSrc || undefined,
|
||||
);
|
||||
}}
|
||||
className="inline-flex w-full items-center justify-center gap-2 rounded-full bg-amber-600 px-4 py-3 text-sm font-bold text-white disabled:opacity-45"
|
||||
>
|
||||
{isBusy ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||
<Sparkles className="h-4 w-4" />
|
||||
{hasFormalImage ? '重新生成画面' : '生成画面'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isHistoryPickerOpen ? (
|
||||
<PuzzleHistoryAssetPickerDialog
|
||||
isBusy={isBusy}
|
||||
@@ -968,6 +960,7 @@ function PuzzleLevelListTab({
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{editState.levels.map((level, index) => {
|
||||
const imageSrc = resolveLevelFormalImageSrc(level);
|
||||
const displayLevelName = level.levelName || `第${index + 1}关`;
|
||||
return (
|
||||
<div
|
||||
key={level.levelId}
|
||||
@@ -983,7 +976,7 @@ function PuzzleLevelListTab({
|
||||
<ResolvedAssetImage
|
||||
src={imageSrc}
|
||||
refreshKey={`${imageRefreshKey}:${level.levelId}`}
|
||||
alt={level.levelName}
|
||||
alt={displayLevelName}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
@@ -996,18 +989,22 @@ function PuzzleLevelListTab({
|
||||
<div className="text-[11px] font-bold tracking-[0.16em] text-[var(--platform-text-soft)]">
|
||||
第{index + 1}关
|
||||
</div>
|
||||
<div className="truncate text-base font-black text-[var(--platform-text-strong)]">
|
||||
{level.levelName || `第${index + 1}关`}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<div className="flex justify-end border-t border-[var(--platform-subpanel-border)] px-3 py-2">
|
||||
<div className="flex items-end gap-2 px-4 pb-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenLevel(level.levelId)}
|
||||
className="min-w-0 flex-1 truncate text-left text-base font-black text-[var(--platform-text-strong)]"
|
||||
>
|
||||
{displayLevelName}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy || editState.levels.length <= 1}
|
||||
onClick={() => onDeleteLevel(level.levelId)}
|
||||
className="platform-icon-button h-9 w-9"
|
||||
aria-label={`删除关卡 ${level.levelName || index + 1}`}
|
||||
className="platform-icon-button h-9 w-9 shrink-0"
|
||||
aria-label={`删除关卡 ${displayLevelName}`}
|
||||
title="删除关卡"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
@@ -1393,6 +1390,7 @@ export function PuzzleResultView({
|
||||
promptText,
|
||||
referenceImageSrc,
|
||||
candidateCount: 1,
|
||||
levelsJson: JSON.stringify(editState.levels),
|
||||
});
|
||||
}}
|
||||
onLevelChange={updateLevel}
|
||||
|
||||
Reference in New Issue
Block a user