Files
Genarrative/src/components/SelectionCustomizationModals.tsx
T
kdletters 1ad25e30f8 收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件
迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome
补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
2026-06-10 10:24:18 +08:00

324 lines
9.0 KiB
TypeScript

import type { ReactNode } from 'react';
import type {
CustomWorldCreatorIntent,
CustomWorldGenerationMode,
} from '../types';
import { PlatformActionButton } from './common/PlatformActionButton';
import { PlatformModalCloseButton } from './common/PlatformModalCloseButton';
import { PlatformProgressBar } from './common/PlatformProgressBar';
import { PlatformStatusMessage } from './common/PlatformStatusMessage';
import { PlatformSubpanel } from './common/PlatformSubpanel';
import {
PlatformSelectField,
PlatformTextField,
} from './common/PlatformTextField';
type BaseModalProps = {
isOpen: boolean;
title: string;
onClose: () => void;
children: ReactNode;
footer?: ReactNode;
};
function SelectionModal({
isOpen,
title,
onClose,
children,
footer = null,
}: BaseModalProps) {
if (!isOpen) return null;
return (
<div className="platform-overlay fixed inset-0 z-[90] flex items-center justify-center p-4 backdrop-blur-sm">
<div className="platform-modal-shell platform-remap-surface flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-3xl shadow-[0_30px_80px_rgba(0,0,0,0.55)]">
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
<div className="text-base font-semibold text-white">{title}</div>
<PlatformModalCloseButton
label={`关闭${title}`}
variant="editorDark"
onClick={onClose}
/>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-5">
{children}
</div>
{footer ? (
<div className="flex flex-wrap items-center justify-end gap-3 border-t border-white/10 px-5 py-4">
{footer}
</div>
) : null}
</div>
</div>
);
}
export function CharacterDraftModal(props: {
isOpen: boolean;
characterLabel: string;
draftName: string;
draftBackstory: string;
onNameChange: (value: string) => void;
onBackstoryChange: (value: string) => void;
onClose: () => void;
onConfirm: () => void;
error?: string | null;
}) {
const {
isOpen,
characterLabel,
draftName,
draftBackstory,
onNameChange,
onBackstoryChange,
onClose,
onConfirm,
error = null,
} = props;
return (
<SelectionModal
isOpen={isOpen}
title="角色自定义"
onClose={onClose}
footer={(
<>
<PlatformActionButton
surface="editorDark"
tone="secondary"
size="sm"
onClick={onClose}
>
取消
</PlatformActionButton>
<PlatformActionButton
surface="editorDark"
tone="success"
size="sm"
onClick={onConfirm}
>
确认进入
</PlatformActionButton>
</>
)}
>
<div className="space-y-4">
<PlatformSubpanel
as="div"
surface="dark"
radius="md"
padding="row"
className="text-sm text-zinc-300"
>
当前角色:{characterLabel}
</PlatformSubpanel>
<label className="block">
<div className="mb-2 text-sm font-medium text-zinc-200">角色名字</div>
<PlatformTextField
value={draftName}
onChange={(event) => onNameChange(event.target.value)}
placeholder="输入一个更贴合这次旅程的称呼"
surface="editorDark"
tone="emerald"
density="roomy"
className="rounded-2xl"
/>
</label>
<label className="block">
<div className="mb-2 text-sm font-medium text-zinc-200">背景补充</div>
<PlatformTextField
variant="textarea"
value={draftBackstory}
onChange={(event) => onBackstoryChange(event.target.value)}
rows={6}
placeholder="可以补充这次开局想强调的身份、经历、执念或禁忌。"
surface="editorDark"
tone="emerald"
size="md"
density="roomy"
className="rounded-2xl leading-7"
/>
</label>
{error ? (
<PlatformStatusMessage
tone="error"
surface="editorDark"
size="md"
className="rounded-2xl"
>
{error}
</PlatformStatusMessage>
) : null}
</div>
</SelectionModal>
);
}
type CustomWorldCreatorModalProps = {
isOpen: boolean;
onClose: () => void;
onSubmit: () => void;
isGenerating: boolean;
progress: number;
progressLabel: string;
error?: string | null;
} & (
| {
draft: string;
onDraftChange: (value: string) => void;
creatorIntent?: never;
onCreatorIntentChange?: never;
generationMode?: never;
onGenerationModeChange?: never;
}
| {
draft?: never;
onDraftChange?: never;
creatorIntent: CustomWorldCreatorIntent;
onCreatorIntentChange: (value: CustomWorldCreatorIntent) => void;
generationMode: CustomWorldGenerationMode;
onGenerationModeChange: (value: CustomWorldGenerationMode) => void;
}
);
function hasCreatorIntentProps(
props: CustomWorldCreatorModalProps,
): props is Extract<
CustomWorldCreatorModalProps,
{ creatorIntent: CustomWorldCreatorIntent }
> {
return 'creatorIntent' in props;
}
export function CustomWorldCreatorModal(props: CustomWorldCreatorModalProps) {
const {
isOpen,
onClose,
onSubmit,
isGenerating,
progress,
progressLabel,
error = null,
} = props;
const draftText = hasCreatorIntentProps(props)
? props.creatorIntent.rawSettingText
: props.draft;
const updateDraftText = (value: string) => {
if (hasCreatorIntentProps(props)) {
props.onCreatorIntentChange({
...props.creatorIntent,
rawSettingText: value,
});
return;
}
props.onDraftChange(value);
};
return (
<SelectionModal
isOpen={isOpen}
title="创建自定义世界"
onClose={onClose}
footer={(
<>
<PlatformActionButton
surface="editorDark"
tone="secondary"
size="sm"
onClick={onClose}
disabled={isGenerating}
>
取消
</PlatformActionButton>
<PlatformActionButton
surface="editorDark"
tone="primary"
size="sm"
onClick={onSubmit}
disabled={isGenerating}
>
{isGenerating ? '生成中...' : '开始生成'}
</PlatformActionButton>
</>
)}
>
<div className="space-y-4">
{hasCreatorIntentProps(props) ? (
<label className="block">
<div className="mb-2 text-sm font-medium text-zinc-200">生成模式</div>
<PlatformSelectField
value={props.generationMode}
onChange={(event) =>
props.onGenerationModeChange(
event.target.value as CustomWorldGenerationMode,
)
}
surface="editorDark"
tone="sky"
density="roomy"
className="rounded-2xl"
>
<option value="fast">快速</option>
<option value="full">完整</option>
</PlatformSelectField>
</label>
) : null}
<div className="text-sm leading-7 text-zinc-300">
用几句话描述世界观、核心矛盾、时代气质和你想体验的叙事方向。系统会据此生成可游玩的自定义世界。
</div>
<PlatformTextField
variant="textarea"
value={draftText}
onChange={(event) => updateDraftText(event.target.value)}
rows={8}
placeholder="例:一个被潮雾与失落列岛切碎的边境世界,旧盟约、沉船秘术与灯塔守望者纠缠在一起……"
surface="editorDark"
tone="sky"
size="md"
density="roomy"
className="rounded-2xl leading-7"
/>
{isGenerating ? (
<PlatformStatusMessage
tone="info"
surface="editorDark"
size="md"
className="rounded-2xl"
>
<div className="mb-2 flex items-center justify-between text-xs tracking-[0.16em] text-sky-100/80">
<span>{progressLabel}</span>
<span>{Math.max(0, Math.min(100, Math.round(progress)))}%</span>
</div>
<PlatformProgressBar
value={progress}
minVisibleValue={6}
ariaLabel="自定义世界生成进度"
className="bg-white/10"
fillClassName="bg-gradient-to-r from-sky-300 to-cyan-200"
/>
</PlatformStatusMessage>
) : null}
{error ? (
<PlatformStatusMessage
tone="error"
surface="editorDark"
size="md"
className="rounded-2xl"
>
{error}
</PlatformStatusMessage>
) : null}
</div>
</SelectionModal>
);
}