Integrate role asset studio into custom world agent flow
This commit is contained in:
702
src/components/custom-world-agent/CustomWorldAgentWorkspace.tsx
Normal file
702
src/components/custom-world-agent/CustomWorldAgentWorkspace.tsx
Normal file
@@ -0,0 +1,702 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import type {
|
||||
CustomWorldAgentActionRequest,
|
||||
CustomWorldAgentOperationRecord,
|
||||
CustomWorldAgentSessionSnapshot,
|
||||
CustomWorldDraftCardDetail,
|
||||
SendCustomWorldAgentMessageRequest,
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import { CustomWorldRoleAssetStudioModal } from '../CustomWorldRoleAssetStudioModal';
|
||||
import { getCustomWorldAgentCardDetail } from '../../services/aiService';
|
||||
import { CustomWorldAgentComposer } from './CustomWorldAgentComposer';
|
||||
import { CustomWorldAgentDraftDetailPanel } from './CustomWorldAgentDraftDetailPanel';
|
||||
import { CustomWorldAgentDraftDrawer } from './CustomWorldAgentDraftDrawer';
|
||||
import { CustomWorldAgentHeader } from './CustomWorldAgentHeader';
|
||||
import { CustomWorldAgentOperationBanner } from './CustomWorldAgentOperationBanner';
|
||||
import { CustomWorldAgentQuickActions } from './CustomWorldAgentQuickActions';
|
||||
import { CustomWorldAgentThread } from './CustomWorldAgentThread';
|
||||
import { CustomWorldDraftCardDetailModal } from './CustomWorldDraftCardDetailModal';
|
||||
import { CustomWorldGenerateEntityModal } from './CustomWorldGenerateEntityModal';
|
||||
|
||||
type WorkspaceRoleAssetTarget = {
|
||||
id: string;
|
||||
name: string;
|
||||
title: string;
|
||||
role: string;
|
||||
description?: string;
|
||||
backstory?: string;
|
||||
personality?: string;
|
||||
motivation?: string;
|
||||
combatStyle?: string;
|
||||
tags?: string[];
|
||||
imageSrc?: string;
|
||||
generatedVisualAssetId?: string;
|
||||
generatedAnimationSetId?: string;
|
||||
animationMap?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type CustomWorldAgentWorkspaceProps = {
|
||||
session: CustomWorldAgentSessionSnapshot | null;
|
||||
activeOperation: CustomWorldAgentOperationRecord | null;
|
||||
onBack: () => void;
|
||||
onRefresh: () => void;
|
||||
onSubmitMessage: (payload: SendCustomWorldAgentMessageRequest) => void;
|
||||
onExecuteAction: (payload: CustomWorldAgentActionRequest) => void;
|
||||
};
|
||||
|
||||
const TOTAL_READINESS_STEPS = 6;
|
||||
const READINESS_ITEMS = [
|
||||
{ key: 'world_hook', label: '世界核心' },
|
||||
{ key: 'player_premise', label: '玩家开局' },
|
||||
{ key: 'theme_and_tone', label: '主题气质' },
|
||||
{ key: 'core_conflict', label: '核心冲突' },
|
||||
{ key: 'relationship_seed', label: '关键关系' },
|
||||
{ key: 'iconic_element', label: '标志元素' },
|
||||
] as const;
|
||||
|
||||
function createClientMessageId() {
|
||||
if (
|
||||
typeof crypto !== 'undefined' &&
|
||||
typeof crypto.randomUUID === 'function'
|
||||
) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
return `client-message-${Date.now()}`;
|
||||
}
|
||||
|
||||
function resolveInitialCardId(session: CustomWorldAgentSessionSnapshot | null) {
|
||||
if (!session || session.draftCards.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
session.focusCardId ||
|
||||
session.draftCards.find((card) => card.kind === 'world')?.id ||
|
||||
session.draftCards[0]?.id ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function buildRecommendedReplies(session: CustomWorldAgentSessionSnapshot) {
|
||||
return session.recommendedReplies;
|
||||
}
|
||||
|
||||
function toRecord(value: unknown) {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function toRecordArray(value: unknown) {
|
||||
return Array.isArray(value)
|
||||
? value.filter(
|
||||
(item): item is Record<string, unknown> =>
|
||||
Boolean(item) && typeof item === 'object' && !Array.isArray(item),
|
||||
)
|
||||
: [];
|
||||
}
|
||||
|
||||
function toText(value: unknown) {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function resolveRoleAssetTarget(
|
||||
session: CustomWorldAgentSessionSnapshot | null,
|
||||
roleId: string | null,
|
||||
) {
|
||||
if (!session || !roleId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const draftProfile = toRecord(session.draftProfile);
|
||||
if (!draftProfile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const playableRole = toRecordArray(draftProfile.playableNpcs).find(
|
||||
(item) => toText(item.id) === roleId,
|
||||
);
|
||||
const storyRole = toRecordArray(draftProfile.storyNpcs).find(
|
||||
(item) => toText(item.id) === roleId,
|
||||
);
|
||||
const role = playableRole ?? storyRole;
|
||||
if (!role) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const assetSummary =
|
||||
session.assetCoverage.roleAssets.find((entry) => entry.roleId === roleId) ??
|
||||
null;
|
||||
|
||||
return {
|
||||
role: {
|
||||
id: roleId,
|
||||
name: toText(role.name) || '未命名角色',
|
||||
title: toText(role.title) || toText(role.role) || '关键角色',
|
||||
role: toText(role.role) || toText(role.title) || '关键角色',
|
||||
description: toText(role.summary),
|
||||
backstory: toText(role.hiddenHook) || undefined,
|
||||
personality: toText(role.publicMask) || undefined,
|
||||
motivation: toText(role.relationToPlayer) || undefined,
|
||||
combatStyle: toText(role.role) || undefined,
|
||||
tags: Array.isArray(role.threadIds)
|
||||
? role.threadIds
|
||||
.map((item) => toText(item))
|
||||
.filter(Boolean)
|
||||
.slice(0, 4)
|
||||
: [],
|
||||
imageSrc: toText(role.imageSrc) || undefined,
|
||||
generatedVisualAssetId: toText(role.generatedVisualAssetId) || undefined,
|
||||
generatedAnimationSetId: toText(role.generatedAnimationSetId) || undefined,
|
||||
animationMap: toRecord(role.animationMap) ?? undefined,
|
||||
} satisfies WorkspaceRoleAssetTarget,
|
||||
roleKind: playableRole ? ('playable' as const) : ('story' as const),
|
||||
assetSummary,
|
||||
};
|
||||
}
|
||||
|
||||
function CustomWorldAgentReadinessBar(props: {
|
||||
completedKeys: string[];
|
||||
isReady: boolean;
|
||||
busy: boolean;
|
||||
onStartDraft: () => void;
|
||||
}) {
|
||||
const { completedKeys, isReady, busy, onStartDraft } = props;
|
||||
const completedKeySet = new Set(completedKeys);
|
||||
const completedCount = READINESS_ITEMS.filter((item) =>
|
||||
completedKeySet.has(item.key),
|
||||
).length;
|
||||
|
||||
return (
|
||||
<div className="rounded-[1.35rem] border border-white/10 bg-[#111318]/95 px-4 py-3">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-xs font-semibold tracking-[0.12em] text-zinc-300">
|
||||
首轮草稿会先确认这 6 项信息
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-zinc-400">
|
||||
{Math.min(completedCount, TOTAL_READINESS_STEPS)}/
|
||||
{TOTAL_READINESS_STEPS}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<div className="grid min-w-0 flex-1 grid-cols-3 gap-2 sm:grid-cols-6">
|
||||
{READINESS_ITEMS.map((item) => (
|
||||
<div
|
||||
key={item.key}
|
||||
className={`rounded-2xl border px-2.5 py-2 text-center text-[11px] ${
|
||||
completedKeySet.has(item.key)
|
||||
? 'border-emerald-300/25 bg-emerald-500/10 text-emerald-100'
|
||||
: 'border-white/10 bg-black/18 text-zinc-500'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3 sm:justify-end">
|
||||
{isReady ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onStartDraft}
|
||||
disabled={busy}
|
||||
className="rounded-full border border-emerald-300/20 bg-emerald-500/10 px-3 py-1.5 text-xs font-medium text-emerald-100 transition hover:text-white disabled:cursor-not-allowed disabled:opacity-45"
|
||||
>
|
||||
{busy ? '生成中' : '开始生成草稿'}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CustomWorldAgentWorkspace({
|
||||
session,
|
||||
activeOperation,
|
||||
onBack,
|
||||
onSubmitMessage,
|
||||
onExecuteAction,
|
||||
}: CustomWorldAgentWorkspaceProps) {
|
||||
const [selectedCardId, setSelectedCardId] = useState<string | null>(() =>
|
||||
resolveInitialCardId(session),
|
||||
);
|
||||
const [detail, setDetail] = useState<CustomWorldDraftCardDetail | null>(null);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [detailModalOpen, setDetailModalOpen] = useState(false);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [autoCompleteConfirmOpen, setAutoCompleteConfirmOpen] = useState(false);
|
||||
const [generateEntityMode, setGenerateEntityMode] = useState<
|
||||
'character' | 'landmark' | null
|
||||
>(null);
|
||||
const [requestedRoleAssetTargetId, setRequestedRoleAssetTargetId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [activeRoleAssetTargetId, setActiveRoleAssetTargetId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [showRoleAssetStudio, setShowRoleAssetStudio] = useState(false);
|
||||
const [closeRoleAssetStudioAfterSync, setCloseRoleAssetStudioAfterSync] =
|
||||
useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session) {
|
||||
setSelectedCardId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const availableCardIds = new Set(session.draftCards.map((card) => card.id));
|
||||
if (session.focusCardId && availableCardIds.has(session.focusCardId)) {
|
||||
setSelectedCardId(session.focusCardId);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedCardId((current) => {
|
||||
if (current && availableCardIds.has(current)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return resolveInitialCardId(session);
|
||||
});
|
||||
}, [session]);
|
||||
|
||||
useEffect(() => {
|
||||
setEditMode(false);
|
||||
}, [detail?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!requestedRoleAssetTargetId || !activeOperation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeOperation.type !== 'generate_role_assets') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeOperation.status === 'completed') {
|
||||
setActiveRoleAssetTargetId(requestedRoleAssetTargetId);
|
||||
setShowRoleAssetStudio(true);
|
||||
setRequestedRoleAssetTargetId(null);
|
||||
setDetailModalOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeOperation.status === 'failed') {
|
||||
setRequestedRoleAssetTargetId(null);
|
||||
}
|
||||
}, [activeOperation, requestedRoleAssetTargetId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeOperation || activeOperation.type !== 'sync_role_assets') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeOperation.status === 'completed') {
|
||||
if (closeRoleAssetStudioAfterSync) {
|
||||
setShowRoleAssetStudio(false);
|
||||
}
|
||||
setCloseRoleAssetStudioAfterSync(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeOperation.status === 'failed') {
|
||||
setCloseRoleAssetStudioAfterSync(false);
|
||||
}
|
||||
}, [activeOperation, closeRoleAssetStudioAfterSync]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session?.sessionId || !selectedCardId) {
|
||||
setDetail(null);
|
||||
setDetailLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setDetailLoading(true);
|
||||
|
||||
void getCustomWorldAgentCardDetail(session.sessionId, selectedCardId)
|
||||
.then((nextDetail) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDetail(nextDetail);
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDetail(null);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [selectedCardId, session?.sessionId, session?.updatedAt]);
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="mx-auto flex h-full w-full max-w-4xl items-center justify-center rounded-[1.75rem] border border-white/10 bg-black/20 px-6 py-8 text-center text-sm text-zinc-400">
|
||||
正在恢复
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isBusy =
|
||||
activeOperation?.status === 'queued' || activeOperation?.status === 'running';
|
||||
const canStartDraft =
|
||||
session.creatorIntentReadiness.isReady &&
|
||||
session.stage === 'foundation_review';
|
||||
const showAutoCompleteButton =
|
||||
!session.creatorIntentReadiness.isReady &&
|
||||
session.creatorIntentReadiness.completedKeys.includes('world_hook');
|
||||
const showDraftWorkspace =
|
||||
(session.stage === 'object_refining' || session.stage === 'visual_refining') &&
|
||||
session.draftCards.length > 0;
|
||||
const selectedCard = session.draftCards.find((card) => card.id === selectedCardId) ?? null;
|
||||
const recommendedReplies = buildRecommendedReplies(session);
|
||||
const selectedRoleAssetContext = resolveRoleAssetTarget(
|
||||
session,
|
||||
activeRoleAssetTargetId,
|
||||
);
|
||||
|
||||
const openRoleAssetStudio = (roleId: string | null) => {
|
||||
if (!roleId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRequestedRoleAssetTargetId(roleId);
|
||||
onExecuteAction({
|
||||
action: 'generate_role_assets',
|
||||
roleIds: [roleId],
|
||||
});
|
||||
};
|
||||
|
||||
const submitTextMessage = (text: string) => {
|
||||
onSubmitMessage({
|
||||
clientMessageId: createClientMessageId(),
|
||||
text,
|
||||
focusCardId: selectedCardId,
|
||||
selectedCardIds: selectedCardId ? [selectedCardId] : [],
|
||||
});
|
||||
};
|
||||
|
||||
const submitSummaryRequest = () => {
|
||||
submitTextMessage(
|
||||
showDraftWorkspace
|
||||
? '帮我总结当前世界底稿,并指出下一步最值得精修的卡片。'
|
||||
: '帮我总结当前设定,并指出下一步最值得补的世界锚点。',
|
||||
);
|
||||
};
|
||||
|
||||
const submitAutoCompleteRequest = () => {
|
||||
submitTextMessage(
|
||||
session.creatorIntentReadiness.isReady
|
||||
? '基于当前设定,帮我自动补强还可以更清晰的细节。'
|
||||
: '请根据当前信息自动补全还缺的设定,并给我一版默认方案。',
|
||||
);
|
||||
setAutoCompleteConfirmOpen(false);
|
||||
};
|
||||
|
||||
const handleRecommendedReply = (reply: string) => {
|
||||
if (canStartDraft && reply.includes('生成草稿')) {
|
||||
onExecuteAction({
|
||||
action: 'draft_foundation',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
submitTextMessage(reply);
|
||||
};
|
||||
|
||||
const openGenerateModal = (mode: 'character' | 'landmark') => {
|
||||
setGenerateEntityMode(mode);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex h-full min-h-0 w-full max-w-[1500px] flex-col gap-3">
|
||||
<CustomWorldAgentHeader onBack={onBack} />
|
||||
<CustomWorldAgentReadinessBar
|
||||
completedKeys={session.creatorIntentReadiness.completedKeys}
|
||||
isReady={canStartDraft}
|
||||
busy={isBusy}
|
||||
onStartDraft={() => {
|
||||
onExecuteAction({
|
||||
action: 'draft_foundation',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<CustomWorldAgentOperationBanner operation={activeOperation} />
|
||||
|
||||
{showDraftWorkspace ? (
|
||||
<div className="grid min-h-0 flex-1 gap-3 xl:grid-cols-[18rem_minmax(0,1fr)_24rem]">
|
||||
<div className="flex min-h-0 flex-col gap-3 xl:overflow-hidden">
|
||||
<CustomWorldAgentQuickActions
|
||||
suggestedActions={session.suggestedActions}
|
||||
disabled={isBusy}
|
||||
canDraftFoundation={canStartDraft}
|
||||
showEntityActions
|
||||
showRoleAssetAction={selectedCard?.kind === 'character'}
|
||||
onRequestSummary={submitSummaryRequest}
|
||||
onDraftFoundation={() => {
|
||||
onExecuteAction({
|
||||
action: 'draft_foundation',
|
||||
});
|
||||
}}
|
||||
onGenerateCharacter={() => {
|
||||
openGenerateModal('character');
|
||||
}}
|
||||
onGenerateLandmark={() => {
|
||||
openGenerateModal('landmark');
|
||||
}}
|
||||
onGenerateRoleAssets={() => {
|
||||
openRoleAssetStudio(selectedCardId);
|
||||
}}
|
||||
onFocusSuggestedAction={(action) => {
|
||||
if (action?.targetId) {
|
||||
setSelectedCardId(action.targetId);
|
||||
setDetailModalOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.draftCards[0]) {
|
||||
setSelectedCardId(session.draftCards[0].id);
|
||||
setDetailModalOpen(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="xl:min-h-0 xl:overflow-y-auto">
|
||||
<CustomWorldAgentDraftDrawer
|
||||
draftCards={session.draftCards}
|
||||
activeCardId={selectedCardId}
|
||||
onSelectCard={(cardId) => {
|
||||
setSelectedCardId(cardId);
|
||||
setDetailModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden min-h-0 xl:block xl:overflow-y-auto">
|
||||
<CustomWorldAgentDraftDetailPanel
|
||||
detail={detail}
|
||||
loading={detailLoading}
|
||||
busy={isBusy}
|
||||
editMode={editMode}
|
||||
onClose={() => {
|
||||
setSelectedCardId(null);
|
||||
setDetailModalOpen(false);
|
||||
setEditMode(false);
|
||||
}}
|
||||
onStartEdit={() => {
|
||||
setEditMode(true);
|
||||
}}
|
||||
onCancelEdit={() => {
|
||||
setEditMode(false);
|
||||
}}
|
||||
onSave={(sections) => {
|
||||
if (!detail) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEditMode(false);
|
||||
onExecuteAction({
|
||||
action: 'update_draft_card',
|
||||
cardId: detail.id,
|
||||
sections,
|
||||
});
|
||||
}}
|
||||
onGenerateCharacter={() => {
|
||||
openGenerateModal('character');
|
||||
}}
|
||||
onGenerateLandmark={() => {
|
||||
openGenerateModal('landmark');
|
||||
}}
|
||||
onOpenRoleAssetStudio={() => {
|
||||
openRoleAssetStudio(detail?.id ?? selectedCardId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-col gap-3">
|
||||
<div className="h-[18rem] min-h-[18rem] xl:min-h-0 xl:flex-1">
|
||||
<CustomWorldAgentThread
|
||||
messages={session.messages}
|
||||
recommendedReplies={recommendedReplies}
|
||||
onRecommendedReply={handleRecommendedReply}
|
||||
/>
|
||||
</div>
|
||||
<CustomWorldAgentComposer
|
||||
disabled={isBusy}
|
||||
onSubmit={onSubmitMessage}
|
||||
onSummaryClick={submitSummaryRequest}
|
||||
onAutoCompleteClick={() => setAutoCompleteConfirmOpen(true)}
|
||||
showAutoComplete={showAutoCompleteButton}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<CustomWorldAgentThread
|
||||
messages={session.messages}
|
||||
recommendedReplies={recommendedReplies}
|
||||
onRecommendedReply={handleRecommendedReply}
|
||||
/>
|
||||
<CustomWorldAgentComposer
|
||||
disabled={isBusy}
|
||||
onSubmit={onSubmitMessage}
|
||||
onSummaryClick={submitSummaryRequest}
|
||||
onAutoCompleteClick={() => setAutoCompleteConfirmOpen(true)}
|
||||
showAutoComplete={showAutoCompleteButton}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{autoCompleteConfirmOpen ? (
|
||||
<div className="fixed inset-0 z-[80] flex items-center justify-center bg-black/70 p-4 backdrop-blur-sm">
|
||||
<div className="w-full max-w-md rounded-[1.5rem] border border-white/10 bg-[#111318] px-5 py-5 shadow-[0_20px_60px_rgba(0,0,0,0.45)]">
|
||||
<div className="text-base font-semibold text-white">
|
||||
自动补全剩余设定
|
||||
</div>
|
||||
<div className="mt-3 text-sm leading-7 text-zinc-300">
|
||||
自动补全会直接给缺失设定填入默认方案,可能降低作品质量。
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAutoCompleteConfirmOpen(false)}
|
||||
className="rounded-full border border-white/10 bg-white/5 px-4 py-2 text-sm text-zinc-200 transition hover:text-white"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={submitAutoCompleteRequest}
|
||||
className="rounded-full border border-emerald-300/20 bg-emerald-500/10 px-4 py-2 text-sm text-emerald-100 transition hover:text-white"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<CustomWorldDraftCardDetailModal
|
||||
open={detailModalOpen}
|
||||
detail={detail}
|
||||
loading={detailLoading}
|
||||
busy={isBusy}
|
||||
editMode={editMode}
|
||||
onClose={() => {
|
||||
setDetailModalOpen(false);
|
||||
setEditMode(false);
|
||||
}}
|
||||
onStartEdit={() => {
|
||||
setEditMode(true);
|
||||
}}
|
||||
onCancelEdit={() => {
|
||||
setEditMode(false);
|
||||
}}
|
||||
onSave={(sections) => {
|
||||
if (!detail) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEditMode(false);
|
||||
setDetailModalOpen(false);
|
||||
onExecuteAction({
|
||||
action: 'update_draft_card',
|
||||
cardId: detail.id,
|
||||
sections,
|
||||
});
|
||||
}}
|
||||
onGenerateCharacter={() => {
|
||||
setDetailModalOpen(false);
|
||||
openGenerateModal('character');
|
||||
}}
|
||||
onGenerateLandmark={() => {
|
||||
setDetailModalOpen(false);
|
||||
openGenerateModal('landmark');
|
||||
}}
|
||||
onOpenRoleAssetStudio={() => {
|
||||
setDetailModalOpen(false);
|
||||
openRoleAssetStudio(detail?.id ?? selectedCardId);
|
||||
}}
|
||||
/>
|
||||
|
||||
<CustomWorldGenerateEntityModal
|
||||
open={generateEntityMode !== null}
|
||||
mode={generateEntityMode ?? 'character'}
|
||||
anchorCardTitle={selectedCard?.title ?? detail?.title ?? null}
|
||||
disabled={isBusy}
|
||||
onClose={() => {
|
||||
setGenerateEntityMode(null);
|
||||
}}
|
||||
onSubmit={({ count, promptText }) => {
|
||||
if (!generateEntityMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
onExecuteAction({
|
||||
action:
|
||||
generateEntityMode === 'character'
|
||||
? 'generate_characters'
|
||||
: 'generate_landmarks',
|
||||
count,
|
||||
promptText: promptText || null,
|
||||
anchorCardIds: selectedCardId ? [selectedCardId] : [],
|
||||
});
|
||||
setGenerateEntityMode(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{showRoleAssetStudio && selectedRoleAssetContext ? (
|
||||
<CustomWorldRoleAssetStudioModal
|
||||
role={selectedRoleAssetContext.role}
|
||||
roleKind={selectedRoleAssetContext.roleKind}
|
||||
priorityTier={
|
||||
selectedRoleAssetContext.assetSummary?.priorityTier ??
|
||||
(selectedRoleAssetContext.roleKind === 'playable'
|
||||
? 'hero'
|
||||
: 'featured')
|
||||
}
|
||||
visualPointCost={
|
||||
selectedRoleAssetContext.assetSummary?.status === 'missing'
|
||||
? selectedRoleAssetContext.assetSummary?.nextPointCost ?? 20
|
||||
: 20
|
||||
}
|
||||
animationPointCost={
|
||||
selectedRoleAssetContext.assetSummary?.status === 'missing'
|
||||
? 60
|
||||
: selectedRoleAssetContext.assetSummary?.nextPointCost ?? 60
|
||||
}
|
||||
syncBusy={
|
||||
activeOperation?.type === 'sync_role_assets' &&
|
||||
(activeOperation.status === 'queued' ||
|
||||
activeOperation.status === 'running')
|
||||
}
|
||||
onPublishSuccess={(payload, options) => {
|
||||
setCloseRoleAssetStudioAfterSync(Boolean(options?.closeAfterSync));
|
||||
onExecuteAction({
|
||||
action: 'sync_role_assets',
|
||||
...payload,
|
||||
});
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowRoleAssetStudio(false);
|
||||
setCloseRoleAssetStudioAfterSync(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user