262 lines
8.2 KiB
TypeScript
262 lines
8.2 KiB
TypeScript
import { useMemo, useState } from 'react';
|
|
|
|
import type { Character, CustomWorldProfile } from '../../types';
|
|
import {
|
|
CustomWorldEntityCatalog,
|
|
type ResultTab,
|
|
} from '../CustomWorldEntityCatalog';
|
|
import RpgCreationEntityEditorModal from '../rpg-creation-editor/RpgCreationEntityEditorModal';
|
|
import RpgCreationAssetDebugPanel, {
|
|
shouldEnableRpgCreationAssetDebugPanel,
|
|
} from './RpgCreationAssetDebugPanel';
|
|
import RpgCreationResultActionBar from './RpgCreationResultActionBar';
|
|
import RpgCreationResultHeader from './RpgCreationResultHeader';
|
|
import { useRpgCreationResultActions } from './useRpgCreationResultActions';
|
|
import type { EntityGenerationKind } from './useRpgCreationResultActions';
|
|
|
|
export interface RpgCreationResultViewProps {
|
|
profile: CustomWorldProfile;
|
|
previewCharacters: Character[];
|
|
isGenerating: boolean;
|
|
progress: number;
|
|
progressLabel: string;
|
|
error: string | null;
|
|
onBack: () => void;
|
|
onEditSetting?: () => void;
|
|
onRegenerate?: () => void;
|
|
onContinueExpand?: () => void;
|
|
onEnterWorld?: () => void;
|
|
onOpenCoverEditor?: () => void;
|
|
onPublishWorld?: () => Promise<void> | void;
|
|
onTestWorld?: () => void;
|
|
onDeleteEntities?: (
|
|
kind: 'story' | 'landmark',
|
|
ids: string[],
|
|
) => Promise<void> | void;
|
|
onGenerateEntity?:
|
|
| ((
|
|
kind: EntityGenerationKind,
|
|
) =>
|
|
| Promise<{ profile?: CustomWorldProfile | null } | void>
|
|
| { profile?: CustomWorldProfile | null }
|
|
| void)
|
|
| undefined;
|
|
onProfileChange: (profile: CustomWorldProfile) => void;
|
|
readOnly?: boolean;
|
|
backLabel?: string;
|
|
editActionLabel?: string;
|
|
regenerateActionLabel?: string;
|
|
enterWorldActionLabel?: string;
|
|
autoSaveState?: 'idle' | 'saving' | 'saved' | 'error';
|
|
compactAgentResultMode?: boolean;
|
|
publishReady?: boolean;
|
|
publishBlockers?: string[];
|
|
qualityFindings?: Array<{
|
|
id: string;
|
|
severity: 'info' | 'warning' | 'blocker';
|
|
code: string;
|
|
targetId?: string | null;
|
|
message: string;
|
|
}>;
|
|
previewSourceLabel?: string | null;
|
|
}
|
|
|
|
export function RpgCreationResultView({
|
|
profile,
|
|
previewCharacters,
|
|
isGenerating,
|
|
progress,
|
|
progressLabel,
|
|
error,
|
|
onBack,
|
|
onEditSetting,
|
|
onRegenerate: triggerRegenerate,
|
|
onContinueExpand,
|
|
onOpenCoverEditor,
|
|
onPublishWorld,
|
|
onTestWorld,
|
|
onDeleteEntities,
|
|
onEnterWorld,
|
|
onGenerateEntity,
|
|
onProfileChange,
|
|
readOnly = false,
|
|
backLabel = '返回',
|
|
editActionLabel = '修改设定',
|
|
regenerateActionLabel = '重新生成',
|
|
enterWorldActionLabel = '进入世界',
|
|
autoSaveState = 'idle',
|
|
compactAgentResultMode = false,
|
|
publishReady = true,
|
|
publishBlockers = [],
|
|
qualityFindings = [],
|
|
}: RpgCreationResultViewProps) {
|
|
const [activeTab, setActiveTab] = useState<ResultTab>('world');
|
|
const assetDebugEnabled = useMemo(
|
|
() => shouldEnableRpgCreationAssetDebugPanel(),
|
|
[],
|
|
);
|
|
const {
|
|
closeEditorTarget,
|
|
createLabel,
|
|
createTarget,
|
|
editorTarget,
|
|
handleDeleteLandmarks,
|
|
handleDeleteStoryNpcs,
|
|
handleGenerateEntity,
|
|
handleRegenerate,
|
|
localGenerationError,
|
|
pendingGeneratedEntity,
|
|
recentGeneratedIds,
|
|
setEditorTarget,
|
|
} = useRpgCreationResultActions({
|
|
activeTab,
|
|
agentEntityGenerator: onGenerateEntity
|
|
? async (kind) => {
|
|
return onGenerateEntity(kind);
|
|
}
|
|
: undefined,
|
|
isGenerating,
|
|
onProfileChange,
|
|
profile,
|
|
readOnly,
|
|
triggerRegenerate,
|
|
});
|
|
|
|
const deleteStoryNpcs = onDeleteEntities
|
|
? (ids: string[]) => {
|
|
void onDeleteEntities('story', ids);
|
|
}
|
|
: handleDeleteStoryNpcs;
|
|
const deleteLandmarks = onDeleteEntities
|
|
? (ids: string[]) => {
|
|
void onDeleteEntities('landmark', ids);
|
|
}
|
|
: handleDeleteLandmarks;
|
|
|
|
return (
|
|
<div className="platform-remap-surface mx-auto flex h-full min-h-0 w-full flex-col xl:max-w-[min(100%,98rem)] xl:px-1 2xl:max-w-[min(100%,112rem)]">
|
|
<RpgCreationResultHeader
|
|
autoSaveState={autoSaveState}
|
|
backLabel={backLabel}
|
|
isGenerating={isGenerating}
|
|
onBack={onBack}
|
|
/>
|
|
|
|
<div className="min-h-0 flex-1 overflow-hidden">
|
|
<CustomWorldEntityCatalog
|
|
profile={profile}
|
|
previewCharacters={previewCharacters}
|
|
activeTab={activeTab}
|
|
onActiveTabChange={setActiveTab}
|
|
onEditTarget={setEditorTarget}
|
|
onProfileChange={onProfileChange}
|
|
onDeleteStoryNpcs={deleteStoryNpcs}
|
|
onDeleteLandmarks={deleteLandmarks}
|
|
createActionLabel={
|
|
readOnly || (compactAgentResultMode && !onGenerateEntity)
|
|
? undefined
|
|
: createLabel
|
|
}
|
|
onCreateAction={
|
|
readOnly ||
|
|
(compactAgentResultMode && !onGenerateEntity) ||
|
|
!createTarget
|
|
? undefined
|
|
: () => {
|
|
if (activeTab === 'playable') {
|
|
void handleGenerateEntity('playable');
|
|
return;
|
|
}
|
|
if (activeTab === 'story') {
|
|
void handleGenerateEntity('story');
|
|
return;
|
|
}
|
|
if (activeTab === 'landmarks') {
|
|
void handleGenerateEntity('landmark');
|
|
return;
|
|
}
|
|
setEditorTarget(createTarget);
|
|
}
|
|
}
|
|
createActionDisabled={Boolean(isGenerating || pendingGeneratedEntity)}
|
|
pendingGeneratedEntity={pendingGeneratedEntity}
|
|
recentGeneratedIds={recentGeneratedIds}
|
|
readOnly={readOnly}
|
|
/>
|
|
</div>
|
|
|
|
{isGenerating && (
|
|
<div className="platform-banner platform-banner--info mt-3 rounded-2xl px-4 py-4">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="text-sm font-semibold text-[var(--platform-text-strong)]">
|
|
{progressLabel}
|
|
</div>
|
|
<div className="text-xs text-[var(--platform-text-base)]">
|
|
{Math.round(progress)}%
|
|
</div>
|
|
</div>
|
|
<div className="platform-progress-track mt-3 h-3 overflow-hidden rounded-full">
|
|
<div
|
|
className="h-full bg-[linear-gradient(90deg,#ff4f8b_0%,#ff8a73_48%,#ffd2a6_100%)] transition-[width] duration-300"
|
|
style={{ width: `${Math.max(0, Math.min(100, progress))}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{error ? (
|
|
<div className="platform-banner platform-banner--danger mt-3 rounded-2xl text-sm leading-6">
|
|
{error}
|
|
</div>
|
|
) : null}
|
|
{!error &&
|
|
compactAgentResultMode &&
|
|
publishBlockers.length <= 0 &&
|
|
qualityFindings.some((entry) => entry.severity === 'warning') ? (
|
|
<div className="platform-banner platform-banner--info mt-3 rounded-2xl text-sm leading-6">
|
|
发布后仍有{' '}
|
|
{
|
|
qualityFindings.filter((entry) => entry.severity === 'warning')
|
|
.length
|
|
}{' '}
|
|
条 warning 可继续优化。
|
|
</div>
|
|
) : null}
|
|
{!error && localGenerationError ? (
|
|
<div className="platform-banner platform-banner--danger mt-3 rounded-2xl text-sm leading-6">
|
|
{localGenerationError}
|
|
</div>
|
|
) : null}
|
|
{assetDebugEnabled ? (
|
|
<RpgCreationAssetDebugPanel profile={profile} />
|
|
) : null}
|
|
|
|
<RpgCreationResultActionBar
|
|
editActionLabel={editActionLabel}
|
|
enterWorldActionLabel={enterWorldActionLabel}
|
|
isGenerating={isGenerating}
|
|
onContinueExpand={onContinueExpand}
|
|
onEditSetting={onEditSetting}
|
|
onEnterWorld={onEnterWorld}
|
|
onOpenCoverEditor={
|
|
onOpenCoverEditor ?? (() => setEditorTarget({ kind: 'cover' }))
|
|
}
|
|
onPublishWorld={onPublishWorld}
|
|
onTestWorld={onTestWorld}
|
|
onRegenerate={triggerRegenerate ? handleRegenerate : undefined}
|
|
profile={profile}
|
|
regenerateActionLabel={regenerateActionLabel}
|
|
publishReady={publishReady}
|
|
publishBlockers={publishBlockers}
|
|
/>
|
|
|
|
<RpgCreationEntityEditorModal
|
|
profile={profile}
|
|
target={editorTarget}
|
|
onClose={closeEditorTarget}
|
|
onProfileChange={onProfileChange}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|