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

324 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}