240 lines
8.9 KiB
TypeScript
240 lines
8.9 KiB
TypeScript
import { AnimatePresence, motion } from 'motion/react';
|
||
import type { ReactNode } from 'react';
|
||
|
||
import { CHROME_ICONS, getNineSliceStyle, UI_CHROME } from '../uiAssets';
|
||
import { PixelIcon } from './PixelIcon';
|
||
|
||
interface CustomWorldCreatorModalProps {
|
||
isOpen: boolean;
|
||
draft: string;
|
||
onDraftChange: (value: string) => void;
|
||
onClose: () => void;
|
||
onSubmit: () => void;
|
||
isGenerating: boolean;
|
||
progress: number;
|
||
progressLabel: string;
|
||
error: string | null;
|
||
}
|
||
|
||
interface CharacterDraftModalProps {
|
||
isOpen: boolean;
|
||
characterLabel: string;
|
||
draftName: string;
|
||
draftBackstory: string;
|
||
onNameChange: (value: string) => void;
|
||
onBackstoryChange: (value: string) => void;
|
||
onClose: () => void;
|
||
onConfirm: () => void;
|
||
error: string | null;
|
||
}
|
||
|
||
function ModalShell({
|
||
isOpen,
|
||
title,
|
||
subtitle,
|
||
onClose,
|
||
disableClose = false,
|
||
children,
|
||
}: {
|
||
isOpen: boolean;
|
||
title: string;
|
||
subtitle?: string;
|
||
onClose: () => void;
|
||
disableClose?: boolean;
|
||
children: ReactNode;
|
||
}) {
|
||
return (
|
||
<AnimatePresence>
|
||
{isOpen && (
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
className="fixed inset-0 z-[90] flex items-center justify-center bg-black/72 p-4 backdrop-blur-sm"
|
||
onClick={disableClose ? undefined : onClose}
|
||
>
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.96, y: 8 }}
|
||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
exit={{ opacity: 0, scale: 0.96, y: 8 }}
|
||
transition={{ duration: 0.18, ease: 'easeOut' }}
|
||
className="pixel-nine-slice pixel-modal-shell flex w-full max-w-2xl flex-col overflow-hidden shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
|
||
style={getNineSliceStyle(UI_CHROME.modalPanel)}
|
||
onClick={event => event.stopPropagation()}
|
||
>
|
||
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
|
||
<div className="min-w-0">
|
||
<div className="text-sm font-semibold text-white">{title}</div>
|
||
{subtitle ? (
|
||
<div className="mt-1 text-xs leading-relaxed text-zinc-400">{subtitle}</div>
|
||
) : null}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={onClose}
|
||
disabled={disableClose}
|
||
className={`rounded-full border border-white/10 bg-black/20 p-2 text-zinc-400 transition-colors hover:text-white ${disableClose ? 'cursor-not-allowed opacity-40' : ''}`}
|
||
>
|
||
<PixelIcon src={CHROME_ICONS.close} className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
<div className="p-5">{children}</div>
|
||
</motion.div>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
);
|
||
}
|
||
|
||
export function CustomWorldCreatorModal({
|
||
isOpen,
|
||
draft,
|
||
onDraftChange,
|
||
onClose,
|
||
onSubmit,
|
||
isGenerating,
|
||
progress,
|
||
progressLabel,
|
||
error,
|
||
}: CustomWorldCreatorModalProps) {
|
||
return (
|
||
<ModalShell
|
||
isOpen={isOpen}
|
||
title="创建自定义世界"
|
||
onClose={onClose}
|
||
disableClose={isGenerating}
|
||
>
|
||
<div className="space-y-4">
|
||
<label className="block">
|
||
<div className="mb-3 text-xs font-bold tracking-[0.16em] text-white">世界设定文本</div>
|
||
<textarea
|
||
value={draft}
|
||
onChange={event => onDraftChange(event.target.value)}
|
||
disabled={isGenerating}
|
||
placeholder="例如:一个被古老机关城与修真宗门共同争夺的边境世界,灵气潮汐会周期性改写地形,玩家需要在多个势力之间周旋,寻找导致世界裂缝扩大的真正原因。"
|
||
className="min-h-[22rem] w-full resize-none rounded-[1.75rem] border border-transparent bg-black/18 px-5 py-4 text-sm leading-7 text-zinc-100 outline-none transition-[background-color,box-shadow] placeholder:text-zinc-500 focus:bg-black/24 focus:shadow-[inset_0_0_0_1px_rgba(125,211,252,0.22)]"
|
||
/>
|
||
</label>
|
||
|
||
{(isGenerating || progress > 0) && (
|
||
<div className="rounded-2xl border border-sky-400/18 bg-sky-500/10 px-4 py-4">
|
||
<div className="flex items-center justify-between gap-3">
|
||
<div className="text-sm font-semibold text-white">{progressLabel}</div>
|
||
<div className="text-xs text-sky-100">{Math.round(progress)}%</div>
|
||
</div>
|
||
<div className="mt-3 h-3 overflow-hidden rounded-full border border-white/10 bg-black/35">
|
||
<div
|
||
className="h-full bg-[linear-gradient(90deg,#38bdf8_0%,#67e8f9_48%,#fef08a_100%)] transition-[width] duration-300"
|
||
style={{ width: `${Math.max(0, Math.min(100, progress))}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{error ? (
|
||
<div className="rounded-2xl border border-rose-400/18 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-100">
|
||
{error}
|
||
</div>
|
||
) : null}
|
||
|
||
<div className="flex justify-end gap-3">
|
||
<button
|
||
type="button"
|
||
onClick={onClose}
|
||
disabled={isGenerating}
|
||
className={`inline-flex min-w-24 items-center justify-center rounded-xl border border-white/10 bg-white/5 px-5 py-2.5 text-sm text-zinc-300 transition-colors hover:bg-white/10 hover:text-white ${isGenerating ? 'cursor-not-allowed opacity-40' : ''}`}
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={onSubmit}
|
||
disabled={isGenerating}
|
||
className={`pixel-nine-slice pixel-pressable text-left ${isGenerating ? 'cursor-wait opacity-60' : ''}`}
|
||
style={getNineSliceStyle(UI_CHROME.choiceButton, { paddingX: 16, paddingY: 10 })}
|
||
>
|
||
<div className="flex items-center justify-between gap-4">
|
||
<span className="text-sm font-semibold text-white">{isGenerating ? '正在生成世界...' : '确认并开始生成'}</span>
|
||
<span className="text-white/60">{isGenerating ? '...' : '→'}</span>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</ModalShell>
|
||
);
|
||
}
|
||
|
||
export function CharacterDraftModal({
|
||
isOpen,
|
||
characterLabel,
|
||
draftName,
|
||
draftBackstory,
|
||
onNameChange,
|
||
onBackstoryChange,
|
||
onClose,
|
||
onConfirm,
|
||
error,
|
||
}: CharacterDraftModalProps) {
|
||
return (
|
||
<ModalShell
|
||
isOpen={isOpen}
|
||
title="自定义角色背景"
|
||
subtitle={`你正在修改 ${characterLabel} 的角色名称与背景故事。`}
|
||
onClose={onClose}
|
||
>
|
||
<div className="space-y-4">
|
||
<div className="rounded-2xl border border-white/8 bg-black/18 px-4 py-3 text-sm leading-7 text-zinc-300">
|
||
这里的修改会直接带入本轮开场、剧情提示词和后续角色展示,不会改动原始预设。
|
||
</div>
|
||
|
||
<label className="block">
|
||
<div className="mb-2 text-xs font-bold tracking-[0.14em] text-white">角色名称</div>
|
||
<input
|
||
value={draftName}
|
||
onChange={event => onNameChange(event.target.value)}
|
||
placeholder="输入新的角色名称"
|
||
className="w-full rounded-2xl border border-white/10 bg-black/25 px-4 py-3 text-sm text-zinc-100 outline-none transition-colors placeholder:text-zinc-500 focus:border-sky-300/35"
|
||
/>
|
||
</label>
|
||
|
||
<label className="block">
|
||
<div className="mb-2 text-xs font-bold tracking-[0.14em] text-white">角色背景故事</div>
|
||
<textarea
|
||
value={draftBackstory}
|
||
onChange={event => onBackstoryChange(event.target.value)}
|
||
placeholder="写下这名角色进入世界前后的经历、动机、执念、秘密或人与人之间的纠葛。"
|
||
className="min-h-44 w-full rounded-2xl border border-white/10 bg-black/25 px-4 py-3 text-sm leading-7 text-zinc-100 outline-none transition-colors placeholder:text-zinc-500 focus:border-sky-300/35"
|
||
/>
|
||
</label>
|
||
|
||
{error ? (
|
||
<div className="rounded-2xl border border-rose-400/18 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-100">
|
||
{error}
|
||
</div>
|
||
) : null}
|
||
|
||
<div className="flex justify-end gap-3">
|
||
<button
|
||
type="button"
|
||
onClick={onClose}
|
||
className="rounded-full border border-white/10 bg-black/20 px-4 py-2 text-sm text-zinc-300 transition-colors hover:text-white"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={onConfirm}
|
||
className="pixel-nine-slice pixel-pressable text-left"
|
||
style={getNineSliceStyle(UI_CHROME.choiceButton, { paddingX: 16, paddingY: 10 })}
|
||
>
|
||
<div className="flex items-center justify-between gap-4">
|
||
<span className="text-sm font-semibold text-white">保存修改</span>
|
||
<span className="text-white/60">→</span>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</ModalShell>
|
||
);
|
||
}
|