chore: checkpoint local workspace changes
This commit is contained in:
@@ -443,7 +443,7 @@ test('readOnly result view hides edit and create actions for agent preview mode'
|
||||
expect(screen.queryByRole('button', { name: /批量删除/u })).toBeNull();
|
||||
});
|
||||
|
||||
test('agent result view keeps publish-enter action clickable and hides sticky publish hints', () => {
|
||||
test('agent result view shows publish blockers and disables publish-enter action', () => {
|
||||
render(
|
||||
<RpgCreationResultView
|
||||
profile={baseProfile}
|
||||
@@ -474,48 +474,15 @@ test('agent result view keeps publish-enter action clickable and hides sticky pu
|
||||
/>,
|
||||
);
|
||||
|
||||
const actionButton = screen.getByRole('button', {
|
||||
name: '发布并进入世界',
|
||||
});
|
||||
expect((actionButton as HTMLButtonElement).disabled).toBe(false);
|
||||
expect(screen.queryByText(/当前结果页数据源:服务端预览/u)).toBeNull();
|
||||
expect(screen.queryByText(/当前还有 2 个发布阻断项/u)).toBeNull();
|
||||
});
|
||||
|
||||
test('agent result view opens publish blocker dialog only when user clicks publish action', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<RpgCreationResultView
|
||||
profile={baseProfile}
|
||||
previewCharacters={[]}
|
||||
isGenerating={false}
|
||||
progress={0}
|
||||
progressLabel=""
|
||||
error={null}
|
||||
onBack={() => {}}
|
||||
onProfileChange={() => {}}
|
||||
compactAgentResultMode
|
||||
publishReady={false}
|
||||
publishBlockers={[
|
||||
'仍有角色缺少正式主图或动作资产,发布前需要先补齐。',
|
||||
'营地还缺少正式场景图资产,发布前需要先确认营地图。',
|
||||
]}
|
||||
previewSourceLabel="服务端预览"
|
||||
enterWorldActionLabel="发布并进入世界"
|
||||
onEnterWorld={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '发布并进入世界' }));
|
||||
|
||||
expect(
|
||||
screen.getByRole('dialog', { name: '发布前检查' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText(/当前还有 2 个阻断项/u)).toBeTruthy();
|
||||
expect(screen.getByText(/当前结果页数据源:服务端预览/u)).toBeTruthy();
|
||||
expect(screen.getByText(/当前还有 2 个发布阻断项/u)).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(/仍有角色缺少正式主图或动作资产/u),
|
||||
).toBeTruthy();
|
||||
const actionButton = screen.getByRole('button', {
|
||||
name: '发布并进入世界',
|
||||
});
|
||||
expect((actionButton as HTMLButtonElement).disabled).toBe(true);
|
||||
});
|
||||
|
||||
test('agent result view keeps publish-enter action enabled when publish gate is clear', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
@@ -20,14 +20,22 @@ afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function ensureScrollApis() {
|
||||
if (!Element.prototype.scrollIntoView) {
|
||||
Element.prototype.scrollIntoView = () => {};
|
||||
}
|
||||
|
||||
if (!HTMLElement.prototype.scrollTo) {
|
||||
HTMLElement.prototype.scrollTo = () => {};
|
||||
}
|
||||
}
|
||||
|
||||
test('creation agent workspace filters duplicate recommended replies', () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined);
|
||||
|
||||
if (!Element.prototype.scrollIntoView) {
|
||||
Element.prototype.scrollIntoView = () => {};
|
||||
}
|
||||
ensureScrollApis();
|
||||
|
||||
render(
|
||||
<CreationAgentWorkspace
|
||||
@@ -78,9 +86,7 @@ test('creation agent workspace filters duplicate recommended replies', () => {
|
||||
});
|
||||
|
||||
test('creation agent workspace renders streaming assistant text', () => {
|
||||
if (!Element.prototype.scrollIntoView) {
|
||||
Element.prototype.scrollIntoView = () => {};
|
||||
}
|
||||
ensureScrollApis();
|
||||
|
||||
render(
|
||||
<CreationAgentWorkspace
|
||||
@@ -114,10 +120,65 @@ test('creation agent workspace renders streaming assistant text', () => {
|
||||
expect(screen.getByText(/那我先顺着这个方向收一下/u)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('creation agent workspace appends streaming assistant message after stable message list', () => {
|
||||
ensureScrollApis();
|
||||
|
||||
render(
|
||||
<CreationAgentWorkspace
|
||||
session={{
|
||||
sessionId: 'creation-agent-session-1',
|
||||
title: '统一共创',
|
||||
currentTurn: 2,
|
||||
progressPercent: 40,
|
||||
anchors: [],
|
||||
messages: [
|
||||
{
|
||||
id: 'message-user-1',
|
||||
role: 'user',
|
||||
kind: 'chat',
|
||||
text: '我想做一个潮湿压抑的海上世界。',
|
||||
},
|
||||
{
|
||||
id: 'message-assistant-1',
|
||||
role: 'assistant',
|
||||
kind: 'chat',
|
||||
text: '我先接住这个方向。',
|
||||
},
|
||||
{
|
||||
id: 'message-user-2',
|
||||
role: 'user',
|
||||
kind: 'chat',
|
||||
text: '开场我想先撞上一场假航灯事故。',
|
||||
},
|
||||
],
|
||||
}}
|
||||
theme={testTheme}
|
||||
loadingText="正在准备"
|
||||
composerPlaceholder="输入消息"
|
||||
primaryActionLabel="生成结果页"
|
||||
streamingReplyText="那我就把开场事故往沉船旧案上收。"
|
||||
isStreamingReply
|
||||
onBack={() => {}}
|
||||
onSubmitText={() => {}}
|
||||
onPrimaryAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const bubbles = screen
|
||||
.getByTestId('creation-agent-message-list')
|
||||
.querySelectorAll('.whitespace-pre-wrap');
|
||||
const bubbleTexts = Array.from(bubbles).map((node) => node.textContent?.trim());
|
||||
|
||||
expect(bubbleTexts).toEqual([
|
||||
'我想做一个潮湿压抑的海上世界。',
|
||||
'我先接住这个方向。',
|
||||
'开场我想先撞上一场假航灯事故。',
|
||||
'那我就把开场事故往沉船旧案上收。',
|
||||
]);
|
||||
});
|
||||
|
||||
test('creation agent workspace hides anchors and primary action before completed progress', () => {
|
||||
if (!Element.prototype.scrollIntoView) {
|
||||
Element.prototype.scrollIntoView = () => {};
|
||||
}
|
||||
ensureScrollApis();
|
||||
|
||||
render(
|
||||
<CreationAgentWorkspace
|
||||
@@ -159,9 +220,7 @@ test('creation agent workspace hides anchors and primary action before completed
|
||||
});
|
||||
|
||||
test('creation agent workspace shows primary and progress actions at completed progress', () => {
|
||||
if (!Element.prototype.scrollIntoView) {
|
||||
Element.prototype.scrollIntoView = () => {};
|
||||
}
|
||||
ensureScrollApis();
|
||||
|
||||
render(
|
||||
<CreationAgentWorkspace
|
||||
@@ -206,26 +265,26 @@ test('creation agent workspace shows primary and progress actions at completed p
|
||||
expect(screen.getByRole('button', { name: '补全剩余设定' })).toBeTruthy();
|
||||
});
|
||||
|
||||
test('creation agent workspace hides hero copy area when title and summary are absent', () => {
|
||||
if (!Element.prototype.scrollIntoView) {
|
||||
Element.prototype.scrollIntoView = () => {};
|
||||
}
|
||||
test('creation agent workspace stops auto-follow when user scrolls away from bottom', () => {
|
||||
ensureScrollApis();
|
||||
|
||||
render(
|
||||
const scrollToSpy = vi.fn();
|
||||
HTMLElement.prototype.scrollTo = scrollToSpy;
|
||||
|
||||
const { rerender } = render(
|
||||
<CreationAgentWorkspace
|
||||
session={{
|
||||
sessionId: 'creation-agent-session-1',
|
||||
title: null,
|
||||
assistantSummary: null,
|
||||
currentTurn: 2,
|
||||
progressPercent: 60,
|
||||
title: '统一共创',
|
||||
currentTurn: 1,
|
||||
progressPercent: 20,
|
||||
anchors: [],
|
||||
messages: [
|
||||
{
|
||||
id: 'message-1',
|
||||
role: 'assistant',
|
||||
kind: 'chat',
|
||||
text: '继续把设定收束到可生成状态。',
|
||||
text: '先确定一下世界方向。',
|
||||
},
|
||||
],
|
||||
}}
|
||||
@@ -239,6 +298,56 @@ test('creation agent workspace hides hero copy area when title and summary are a
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('统一共创')).toBeNull();
|
||||
expect(screen.getByText('创作进度')).toBeTruthy();
|
||||
const messageList = screen.getByTestId('creation-agent-message-list');
|
||||
let scrollTop = 120;
|
||||
|
||||
Object.defineProperty(messageList, 'scrollHeight', {
|
||||
configurable: true,
|
||||
value: 640,
|
||||
});
|
||||
Object.defineProperty(messageList, 'clientHeight', {
|
||||
configurable: true,
|
||||
value: 240,
|
||||
});
|
||||
Object.defineProperty(messageList, 'scrollTop', {
|
||||
configurable: true,
|
||||
get: () => scrollTop,
|
||||
set: (value) => {
|
||||
scrollTop = Number(value);
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.scroll(messageList);
|
||||
scrollToSpy.mockClear();
|
||||
|
||||
rerender(
|
||||
<CreationAgentWorkspace
|
||||
session={{
|
||||
sessionId: 'creation-agent-session-1',
|
||||
title: '统一共创',
|
||||
currentTurn: 1,
|
||||
progressPercent: 20,
|
||||
anchors: [],
|
||||
messages: [
|
||||
{
|
||||
id: 'message-1',
|
||||
role: 'assistant',
|
||||
kind: 'chat',
|
||||
text: '先确定一下世界方向。',
|
||||
},
|
||||
],
|
||||
}}
|
||||
theme={testTheme}
|
||||
loadingText="正在准备"
|
||||
composerPlaceholder="输入消息"
|
||||
primaryActionLabel="生成结果页"
|
||||
streamingReplyText="继续往下收束开场冲突。"
|
||||
isStreamingReply
|
||||
onBack={() => {}}
|
||||
onSubmitText={() => {}}
|
||||
onPrimaryAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(scrollToSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ export type CreationAgentOperationView = {
|
||||
|
||||
export type CreationAgentSessionView = {
|
||||
sessionId: string;
|
||||
title?: string | null;
|
||||
title: string;
|
||||
assistantSummary?: string | null;
|
||||
currentTurn: number;
|
||||
progressPercent: number;
|
||||
@@ -79,6 +79,8 @@ type CreationAgentWorkspaceProps = {
|
||||
onQuickAction?: (action: CreationAgentQuickAction) => void;
|
||||
};
|
||||
|
||||
const AUTO_SCROLL_FOLLOW_THRESHOLD_PX = 96;
|
||||
|
||||
function uniqueRecommendedReplies(recommendedReplies: string[] = []) {
|
||||
return [...new Set(recommendedReplies.map((reply) => reply.trim()).filter(Boolean))].slice(
|
||||
0,
|
||||
@@ -165,16 +167,18 @@ function CreationAgentMessageBubble({
|
||||
message,
|
||||
theme,
|
||||
recommendedReplies,
|
||||
isStreaming = false,
|
||||
onRecommendedReply,
|
||||
}: {
|
||||
message: CreationAgentMessageView;
|
||||
theme: CreationAgentTheme;
|
||||
recommendedReplies?: string[];
|
||||
isStreaming?: boolean;
|
||||
onRecommendedReply: (text: string) => void;
|
||||
}) {
|
||||
const isUser = message.role === 'user';
|
||||
const isSystem = message.role === 'system';
|
||||
const visibleRecommendedReplies = isUser
|
||||
const visibleRecommendedReplies = isUser || isStreaming
|
||||
? []
|
||||
: uniqueRecommendedReplies(recommendedReplies);
|
||||
const bubbleToneClass = isUser
|
||||
@@ -188,7 +192,24 @@ function CreationAgentMessageBubble({
|
||||
<div
|
||||
className={`max-w-[90%] rounded-[1.4rem] px-4 py-3 text-sm leading-7 break-words sm:max-w-[82%] ${bubbleToneClass}`}
|
||||
>
|
||||
<div className="whitespace-pre-wrap">{message.text}</div>
|
||||
{isStreaming ? (
|
||||
message.text ? (
|
||||
<div className="whitespace-pre-wrap">
|
||||
{message.text}
|
||||
<span
|
||||
className={`ml-1 inline-block h-4 w-1 animate-pulse rounded-full align-[-2px] ${theme.accentBgClass}`}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 py-1">
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-[var(--platform-text-soft)] [animation-delay:-0.2s]" />
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-[var(--platform-text-soft)] [animation-delay:-0.1s]" />
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-[var(--platform-text-soft)]" />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="whitespace-pre-wrap">{message.text}</div>
|
||||
)}
|
||||
{visibleRecommendedReplies.length > 0 ? (
|
||||
<div className="mt-2.5 flex flex-col gap-1.5">
|
||||
{visibleRecommendedReplies.map((reply, replyIndex) => (
|
||||
@@ -228,6 +249,25 @@ function shouldShowQuickAction(
|
||||
return true;
|
||||
}
|
||||
|
||||
function isMessageListNearBottom(container: HTMLDivElement) {
|
||||
return (
|
||||
container.scrollHeight - container.scrollTop - container.clientHeight <=
|
||||
AUTO_SCROLL_FOLLOW_THRESHOLD_PX
|
||||
);
|
||||
}
|
||||
|
||||
function scrollMessageListToBottom(container: HTMLDivElement) {
|
||||
if (typeof container.scrollTo === 'function') {
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'auto',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
export function CreationAgentWorkspace({
|
||||
session,
|
||||
theme,
|
||||
@@ -247,14 +287,18 @@ export function CreationAgentWorkspace({
|
||||
onQuickAction,
|
||||
}: CreationAgentWorkspaceProps) {
|
||||
const [draftText, setDraftText] = useState('');
|
||||
const bottomRef = useRef<HTMLDivElement | null>(null);
|
||||
// 统一聊天区只在用户仍停留在底部附近时跟随新内容,避免流式回复持续抢走阅读位置。
|
||||
const messageListRef = useRef<HTMLDivElement | null>(null);
|
||||
const shouldAutoScrollRef = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
bottomRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'end',
|
||||
});
|
||||
}, [session?.messages, streamingReplyText, isStreamingReply]);
|
||||
const container = messageListRef.current;
|
||||
if (!container || !shouldAutoScrollRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollMessageListToBottom(container);
|
||||
}, [session?.sessionId, session?.messages, streamingReplyText, isStreamingReply]);
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
@@ -267,23 +311,53 @@ export function CreationAgentWorkspace({
|
||||
}
|
||||
|
||||
const progress = normalizeCreationAgentProgress(session.progressPercent);
|
||||
const hasHeroCopy = Boolean(session.title || session.assistantSummary);
|
||||
const canShowPrimaryAction = progress >= 100;
|
||||
const visibleQuickActions = quickActions.filter((action) =>
|
||||
shouldShowQuickAction(action, session, progress),
|
||||
);
|
||||
const streamingMessageId = `streaming-assistant-${session.sessionId}`;
|
||||
const displayedMessages = isStreamingReply
|
||||
? [
|
||||
...session.messages,
|
||||
{
|
||||
id: streamingMessageId,
|
||||
role: 'assistant',
|
||||
kind: 'chat',
|
||||
text: streamingReplyText,
|
||||
} satisfies CreationAgentMessageView,
|
||||
]
|
||||
: session.messages;
|
||||
const lastAssistantMessageIndex = session.messages.reduce(
|
||||
(lastIndex, message, index) =>
|
||||
message.role === 'assistant' ? index : lastIndex,
|
||||
-1,
|
||||
);
|
||||
|
||||
const armAutoScrollToBottom = () => {
|
||||
shouldAutoScrollRef.current = true;
|
||||
};
|
||||
|
||||
const handleMessageListScroll = () => {
|
||||
const container = messageListRef.current;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
shouldAutoScrollRef.current = isMessageListNearBottom(container);
|
||||
};
|
||||
|
||||
const submitRecommendedReply = (text: string) => {
|
||||
armAutoScrollToBottom();
|
||||
onSubmitText(text);
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
const text = draftText.trim();
|
||||
if (!text || isBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
armAutoScrollToBottom();
|
||||
onSubmitText(text);
|
||||
setDraftText('');
|
||||
};
|
||||
@@ -314,22 +388,18 @@ export function CreationAgentWorkspace({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{hasHeroCopy ? (
|
||||
<div className="mt-6">
|
||||
{session.title ? (
|
||||
<div className="text-2xl font-black leading-tight sm:text-3xl">
|
||||
{session.title}
|
||||
</div>
|
||||
) : null}
|
||||
{session.assistantSummary ? (
|
||||
<div className="mt-2 max-w-2xl text-sm leading-6 text-white/76">
|
||||
{session.assistantSummary}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-6">
|
||||
<div className="text-2xl font-black leading-tight sm:text-3xl">
|
||||
{session.title}
|
||||
</div>
|
||||
) : null}
|
||||
{session.assistantSummary ? (
|
||||
<div className="mt-2 max-w-2xl text-sm leading-6 text-white/76">
|
||||
{session.assistantSummary}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className={hasHeroCopy ? 'mt-4' : 'mt-6'}>
|
||||
<div className="mt-4">
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<span className="text-xs font-semibold tracking-[0.14em] text-white/72">
|
||||
创作进度
|
||||
@@ -370,48 +440,33 @@ export function CreationAgentWorkspace({
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-hidden rounded-[1.6rem] border border-[var(--platform-subpanel-border)] bg-[var(--platform-subpanel-fill)]">
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<div className="min-h-0 flex-1 space-y-3 overflow-y-auto px-4 py-4">
|
||||
{session.messages.length === 0 ? (
|
||||
<div
|
||||
ref={messageListRef}
|
||||
data-testid="creation-agent-message-list"
|
||||
onScroll={handleMessageListScroll}
|
||||
className="min-h-0 flex-1 space-y-3 overflow-y-auto px-4 py-4"
|
||||
>
|
||||
{displayedMessages.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center text-sm text-[var(--platform-text-soft)]">
|
||||
暂无消息
|
||||
</div>
|
||||
) : (
|
||||
session.messages.map((message, index) => (
|
||||
displayedMessages.map((message, index) => (
|
||||
<CreationAgentMessageBubble
|
||||
key={message.id || `message-${index}`}
|
||||
message={message}
|
||||
theme={theme}
|
||||
isStreaming={message.id === streamingMessageId}
|
||||
recommendedReplies={
|
||||
message.id !== streamingMessageId &&
|
||||
index === lastAssistantMessageIndex
|
||||
? session.recommendedReplies
|
||||
: []
|
||||
}
|
||||
onRecommendedReply={(text) => onSubmitText(text)}
|
||||
onRecommendedReply={submitRecommendedReply}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
{isStreamingReply ? (
|
||||
<div className="flex justify-start">
|
||||
<div className="platform-subpanel max-w-[90%] rounded-[1.4rem] px-4 py-3 text-sm leading-7 text-[var(--platform-text-strong)] sm:max-w-[82%]">
|
||||
{streamingReplyText ? (
|
||||
<div className="whitespace-pre-wrap">
|
||||
{streamingReplyText}
|
||||
<span
|
||||
className={`ml-1 inline-block h-4 w-1 animate-pulse rounded-full align-[-2px] ${theme.accentBgClass}`}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 py-1">
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-[var(--platform-text-soft)] [animation-delay:-0.2s]" />
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-[var(--platform-text-soft)] [animation-delay:-0.1s]" />
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-[var(--platform-text-soft)]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
|
||||
@@ -77,10 +77,6 @@ test('custom world agent workspace renders minimum loop chat layout', () => {
|
||||
expect(html).toContain('输入消息');
|
||||
expect(html).toContain('总结当前设定');
|
||||
expect(html).toContain('补全剩余设定');
|
||||
expect(html).not.toContain('世界共创');
|
||||
expect(html).not.toContain(
|
||||
'先说一个你最想让玩家记住的世界方向,我会帮你收束成可生成草稿的锚点。',
|
||||
);
|
||||
expect(html).not.toContain('Agent');
|
||||
expect(html).not.toContain('刷新');
|
||||
expect(html).not.toContain('当前轮次');
|
||||
|
||||
@@ -83,9 +83,10 @@ function mapCustomWorldSession(
|
||||
): CreationAgentSessionView {
|
||||
return {
|
||||
sessionId: session.sessionId,
|
||||
// 自定义世界 Agent 聊天页顶部保持纯操作区,不额外显示标题和引导副文案。
|
||||
title: null,
|
||||
assistantSummary: null,
|
||||
title: '世界共创',
|
||||
assistantSummary:
|
||||
session.lastAssistantReply ||
|
||||
'先说一个你最想让玩家记住的世界方向,我会帮你收束成可生成草稿的锚点。',
|
||||
currentTurn: session.currentTurn,
|
||||
progressPercent: session.progressPercent,
|
||||
anchors: [
|
||||
|
||||
@@ -958,6 +958,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
if (!work.profileId) {
|
||||
platformBootstrap.setPlatformError('当前作品缺少 profileId,暂时无法删除。');
|
||||
return;
|
||||
}
|
||||
|
||||
setDeletingCreationWorkId(work.workId);
|
||||
platformBootstrap.setPlatformError(null);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { X } from 'lucide-react';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
|
||||
@@ -31,81 +29,6 @@ function SmallButton({
|
||||
);
|
||||
}
|
||||
|
||||
function PublishBlockersDialog({
|
||||
blockers,
|
||||
onClose,
|
||||
}: {
|
||||
blockers: string[];
|
||||
onClose: () => void;
|
||||
}) {
|
||||
if (typeof document === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className="platform-overlay fixed inset-0 z-[140] flex items-end justify-center bg-slate-950/56 p-3 backdrop-blur-sm sm:items-center sm:p-4"
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="发布前检查"
|
||||
className="platform-modal-shell platform-remap-surface flex max-h-[min(88vh,42rem)] w-full max-w-lg flex-col overflow-hidden rounded-t-[1.75rem] shadow-[0_24px_80px_rgba(0,0,0,0.55)] sm:rounded-[1.75rem]"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 border-b border-white/10 px-5 py-4">
|
||||
<div>
|
||||
<div className="text-base font-semibold text-white">
|
||||
发布前还需要补齐这些内容
|
||||
</div>
|
||||
<div className="mt-1 text-sm leading-6 text-[var(--platform-text-soft)]">
|
||||
当前还有 {blockers.length} 个阻断项,补齐后再发布并进入世界。
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="关闭"
|
||||
className="platform-icon-button"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
||||
<div className="space-y-2">
|
||||
{blockers.map((blocker, index) => (
|
||||
<div
|
||||
key={`publish-blocker-${index}-${blocker}`}
|
||||
className="rounded-[1.1rem] border border-amber-300/18 bg-amber-500/10 px-4 py-3 text-sm leading-6 text-[var(--platform-text-strong)]"
|
||||
>
|
||||
<div className="text-xs font-semibold tracking-[0.14em] text-amber-100/78">
|
||||
阻断项 {index + 1}
|
||||
</div>
|
||||
<div className="mt-1">{blocker}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end border-t border-white/10 px-5 py-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="platform-button platform-button--primary"
|
||||
>
|
||||
我知道了
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
interface RpgCreationResultActionBarProps {
|
||||
editActionLabel: string;
|
||||
enterWorldActionLabel: string;
|
||||
@@ -117,7 +40,6 @@ interface RpgCreationResultActionBarProps {
|
||||
profile: CustomWorldProfile;
|
||||
regenerateActionLabel: string;
|
||||
publishReady: boolean;
|
||||
publishBlockers: string[];
|
||||
}
|
||||
|
||||
export function RpgCreationResultActionBar({
|
||||
@@ -131,21 +53,7 @@ export function RpgCreationResultActionBar({
|
||||
profile,
|
||||
regenerateActionLabel,
|
||||
publishReady,
|
||||
publishBlockers,
|
||||
}: RpgCreationResultActionBarProps) {
|
||||
const [showPublishBlockersDialog, setShowPublishBlockersDialog] =
|
||||
useState(false);
|
||||
|
||||
// 结果页保持清爽,只有用户发起发布动作时才弹出阻断项提示。
|
||||
const handleEnterWorld = () => {
|
||||
if (!publishReady) {
|
||||
setShowPublishBlockersDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
onEnterWorld?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex flex-col gap-3">
|
||||
{profile.generationStatus === 'key_only' ? (
|
||||
@@ -174,24 +82,14 @@ export function RpgCreationResultActionBar({
|
||||
{onEnterWorld ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleEnterWorld}
|
||||
disabled={isGenerating}
|
||||
onClick={onEnterWorld}
|
||||
disabled={isGenerating || !publishReady}
|
||||
className={`platform-button platform-button--primary ${isGenerating ? 'opacity-55' : ''}`}
|
||||
>
|
||||
{enterWorldActionLabel}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{showPublishBlockersDialog ? (
|
||||
<PublishBlockersDialog
|
||||
blockers={
|
||||
publishBlockers.length > 0
|
||||
? publishBlockers
|
||||
: ['当前草稿还没有通过发布门槛,请先补齐必要内容。']
|
||||
}
|
||||
onClose={() => setShowPublishBlockersDialog(false)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ export function RpgCreationResultView({
|
||||
publishReady = true,
|
||||
publishBlockers = [],
|
||||
qualityFindings = [],
|
||||
previewSourceLabel = null,
|
||||
}: RpgCreationResultViewProps) {
|
||||
const [activeTab, setActiveTab] = useState<ResultTab>('world');
|
||||
const assetDebugEnabled = useMemo(
|
||||
@@ -170,6 +171,25 @@ export function RpgCreationResultView({
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
{!error && compactAgentResultMode && previewSourceLabel ? (
|
||||
<div className="platform-banner platform-banner--info mt-3 rounded-2xl text-sm leading-6">
|
||||
当前结果页数据源:{previewSourceLabel}
|
||||
</div>
|
||||
) : null}
|
||||
{!error && compactAgentResultMode && publishBlockers.length > 0 ? (
|
||||
<div className="platform-banner platform-banner--warning mt-3 rounded-2xl text-sm leading-6">
|
||||
{publishReady
|
||||
? '当前世界已满足发布门槛。'
|
||||
: `当前还有 ${publishBlockers.length} 个发布阻断项,请先补齐后再进入世界。`}
|
||||
<div className="mt-2 space-y-1">
|
||||
{publishBlockers.slice(0, 4).map((entry, index) => (
|
||||
<div key={`publish-blocker-${index}-${entry}`}>
|
||||
{index + 1}. {entry}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{!error &&
|
||||
compactAgentResultMode &&
|
||||
publishBlockers.length <= 0 &&
|
||||
@@ -196,7 +216,6 @@ export function RpgCreationResultView({
|
||||
profile={profile}
|
||||
regenerateActionLabel={regenerateActionLabel}
|
||||
publishReady={publishReady}
|
||||
publishBlockers={publishBlockers}
|
||||
/>
|
||||
|
||||
<RpgCreationEntityEditorModal
|
||||
|
||||
@@ -598,7 +598,7 @@ beforeEach(() => {
|
||||
sessionId: 'puzzle-session-1',
|
||||
currentTurn: 0,
|
||||
progressPercent: 0,
|
||||
stage: 'clarifying',
|
||||
stage: 'collecting_anchors',
|
||||
anchorPack: {
|
||||
themePromise: {
|
||||
key: 'theme_promise',
|
||||
@@ -691,7 +691,11 @@ test('create hub exposes direct template entry, keeps AIRP and visual novel lock
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByText('Agent工作区:custom-world-agent-session-1'),
|
||||
await screen.findByText(
|
||||
'Agent工作区:custom-world-agent-session-1',
|
||||
{},
|
||||
{ timeout: 5000 },
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -1097,7 +1101,7 @@ test('existing draft sessions open result page refinement instead of agent dialo
|
||||
expect(screen.getByRole('button', { name: /AI生成/u })).toBeTruthy();
|
||||
});
|
||||
|
||||
test('agent result view shows publish blocker dialog before publish action when preview gate is not ready', async () => {
|
||||
test('agent result view shows publish blockers and disables publish-enter action when preview gate is not ready', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(getRpgCreationOperation).mockResolvedValue({
|
||||
@@ -1128,33 +1132,11 @@ test('agent result view shows publish blocker dialog before publish action when
|
||||
|
||||
await openNewRpgCreation(user);
|
||||
|
||||
const actionButton = await screen.findByRole('button', {
|
||||
expect(await screen.findByText(/当前还有 1 个发布阻断项/u)).toBeTruthy();
|
||||
const actionButton = screen.getByRole('button', {
|
||||
name: /发布并进入世界/u,
|
||||
});
|
||||
expect((actionButton as HTMLButtonElement).disabled).toBe(false);
|
||||
|
||||
const publishWorldCallCountBeforeClick = vi
|
||||
.mocked(executeRpgCreationAction)
|
||||
.mock.calls.filter(
|
||||
([sessionId, payload]) =>
|
||||
sessionId === 'custom-world-agent-session-1' &&
|
||||
payload?.action === 'publish_world',
|
||||
).length;
|
||||
|
||||
await user.click(actionButton);
|
||||
|
||||
expect(await screen.findByRole('dialog', { name: '发布前检查' })).toBeTruthy();
|
||||
expect(screen.getByText(/当前还有 1 个阻断项/u)).toBeTruthy();
|
||||
expect(screen.getByText(/仍有角色缺少正式主图或动作资产/u)).toBeTruthy();
|
||||
|
||||
const publishWorldCallCountAfterClick = vi
|
||||
.mocked(executeRpgCreationAction)
|
||||
.mock.calls.filter(
|
||||
([sessionId, payload]) =>
|
||||
sessionId === 'custom-world-agent-session-1' &&
|
||||
payload?.action === 'publish_world',
|
||||
).length;
|
||||
expect(publishWorldCallCountAfterClick).toBe(publishWorldCallCountBeforeClick);
|
||||
expect((actionButton as HTMLButtonElement).disabled).toBe(true);
|
||||
});
|
||||
|
||||
test('agent draft result publishes before entering world and uses published preview profile', async () => {
|
||||
|
||||
@@ -46,6 +46,11 @@ type UseRpgCreationSessionControllerParams = {
|
||||
onSessionOpened?: (() => void) | undefined;
|
||||
};
|
||||
|
||||
type PendingAgentUserMessage = {
|
||||
sessionId: string;
|
||||
message: CustomWorldAgentSessionSnapshot['messages'][number];
|
||||
};
|
||||
|
||||
export function useRpgCreationSessionController(
|
||||
params: UseRpgCreationSessionControllerParams,
|
||||
) {
|
||||
@@ -64,6 +69,8 @@ export function useRpgCreationSessionController(
|
||||
const hasAppliedInitialAgentWorkspaceRef = useRef(false);
|
||||
const hasRequestedInitialAgentWorkspaceAuthRef = useRef(false);
|
||||
const isAgentDraftResultAutoOpenSuppressedRef = useRef(false);
|
||||
const currentAgentSessionIdRef = useRef<string | null>(null);
|
||||
const latestAgentSessionSyncRequestIdRef = useRef(0);
|
||||
|
||||
const [isCreatingAgentSession, setIsCreatingAgentSession] = useState(false);
|
||||
const [activeAgentSessionId, setActiveAgentSessionId] = useState<
|
||||
@@ -78,6 +85,8 @@ export function useRpgCreationSessionController(
|
||||
useState<CustomWorldAgentOperationRecord | null>(null);
|
||||
const [streamingAgentReplyText, setStreamingAgentReplyText] = useState('');
|
||||
const [isStreamingAgentReply, setIsStreamingAgentReply] = useState(false);
|
||||
const [pendingAgentUserMessage, setPendingAgentUserMessage] =
|
||||
useState<PendingAgentUserMessage | null>(null);
|
||||
const [isLoadingAgentSession, setIsLoadingAgentSession] = useState(false);
|
||||
const [creationTypeError, setCreationTypeError] = useState<string | null>(null);
|
||||
const [agentWorkspaceRestoreError, setAgentWorkspaceRestoreError] =
|
||||
@@ -91,6 +100,44 @@ export function useRpgCreationSessionController(
|
||||
useState<CustomWorldResultViewSource>(null);
|
||||
const [agentDraftGenerationStartedAt, setAgentDraftGenerationStartedAt] =
|
||||
useState<number | null>(null);
|
||||
const pendingAgentUserMessageRef = useRef<PendingAgentUserMessage | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
currentAgentSessionIdRef.current = agentSession?.sessionId ?? null;
|
||||
}, [agentSession]);
|
||||
|
||||
useEffect(() => {
|
||||
pendingAgentUserMessageRef.current = pendingAgentUserMessage;
|
||||
}, [pendingAgentUserMessage]);
|
||||
|
||||
const invalidateAgentSessionSyncRequests = useCallback(() => {
|
||||
latestAgentSessionSyncRequestIdRef.current += 1;
|
||||
}, []);
|
||||
|
||||
const mergePendingAgentUserMessageIntoSession = useCallback(
|
||||
(
|
||||
session: CustomWorldAgentSessionSnapshot | null,
|
||||
pending: PendingAgentUserMessage | null = pendingAgentUserMessageRef.current,
|
||||
) => {
|
||||
if (!session || !pending || pending.sessionId !== session.sessionId) {
|
||||
return session;
|
||||
}
|
||||
|
||||
const hasServerEchoedPendingMessage = session.messages.some(
|
||||
(message) => message.id === pending.message.id,
|
||||
);
|
||||
if (hasServerEchoedPendingMessage) {
|
||||
return session;
|
||||
}
|
||||
|
||||
return {
|
||||
...session,
|
||||
messages: [...session.messages, pending.message],
|
||||
updatedAt: pending.message.createdAt,
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const persistAgentUiState = useCallback(
|
||||
(nextSessionId: string | null, nextOperationId: string | null) => {
|
||||
@@ -105,10 +152,26 @@ export function useRpgCreationSessionController(
|
||||
);
|
||||
|
||||
const syncAgentSessionSnapshot = useCallback(async (sessionId: string) => {
|
||||
const requestId = latestAgentSessionSyncRequestIdRef.current + 1;
|
||||
latestAgentSessionSyncRequestIdRef.current = requestId;
|
||||
const nextSession = await getRpgCreationSession(sessionId);
|
||||
setAgentSession(nextSession);
|
||||
return nextSession;
|
||||
}, []);
|
||||
const mergedSession = mergePendingAgentUserMessageIntoSession(nextSession);
|
||||
|
||||
if (latestAgentSessionSyncRequestIdRef.current === requestId) {
|
||||
setAgentSession(mergedSession);
|
||||
const currentPendingAgentUserMessage = pendingAgentUserMessageRef.current;
|
||||
const hasServerEchoedPendingMessage =
|
||||
currentPendingAgentUserMessage?.sessionId === nextSession.sessionId &&
|
||||
nextSession.messages.some(
|
||||
(message) => message.id === currentPendingAgentUserMessage.message.id,
|
||||
);
|
||||
if (hasServerEchoedPendingMessage) {
|
||||
setPendingAgentUserMessage(null);
|
||||
}
|
||||
}
|
||||
|
||||
return mergedSession;
|
||||
}, [mergePendingAgentUserMessageIntoSession]);
|
||||
|
||||
useEffect(() => {
|
||||
const initialAgentSessionId = initialAgentUiStateRef.current.activeSessionId;
|
||||
@@ -135,22 +198,26 @@ export function useRpgCreationSessionController(
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeAgentSessionId) {
|
||||
invalidateAgentSessionSyncRequests();
|
||||
setAgentSession(null);
|
||||
setAgentOperation(null);
|
||||
setIsLoadingAgentSession(false);
|
||||
setStreamingAgentReplyText('');
|
||||
setIsStreamingAgentReply(false);
|
||||
setPendingAgentUserMessage(null);
|
||||
setAgentWorkspaceRestoreError(null);
|
||||
isHydratingInitialAgentWorkspaceRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
invalidateAgentSessionSyncRequests();
|
||||
setAgentSession(null);
|
||||
setAgentOperation(null);
|
||||
setIsLoadingAgentSession(false);
|
||||
setStreamingAgentReplyText('');
|
||||
setIsStreamingAgentReply(false);
|
||||
setPendingAgentUserMessage(null);
|
||||
setAgentWorkspaceRestoreError(null);
|
||||
return;
|
||||
}
|
||||
@@ -159,6 +226,15 @@ export function useRpgCreationSessionController(
|
||||
const isInitialWorkspaceRestore =
|
||||
isHydratingInitialAgentWorkspaceRef.current &&
|
||||
activeAgentSessionId === initialAgentUiStateRef.current.activeSessionId;
|
||||
|
||||
if (currentAgentSessionIdRef.current !== activeAgentSessionId) {
|
||||
setAgentSession(null);
|
||||
setAgentOperation(null);
|
||||
setStreamingAgentReplyText('');
|
||||
setIsStreamingAgentReply(false);
|
||||
setPendingAgentUserMessage(null);
|
||||
}
|
||||
|
||||
setIsLoadingAgentSession(true);
|
||||
|
||||
void syncAgentSessionSnapshot(activeAgentSessionId)
|
||||
@@ -204,6 +280,7 @@ export function useRpgCreationSessionController(
|
||||
}, [
|
||||
activeAgentSessionId,
|
||||
enterCreateTab,
|
||||
invalidateAgentSessionSyncRequests,
|
||||
persistAgentUiState,
|
||||
setSelectionStage,
|
||||
syncAgentSessionSnapshot,
|
||||
@@ -368,6 +445,17 @@ export function useRpgCreationSessionController(
|
||||
setIsCreatingAgentSession(true);
|
||||
setCreationTypeError(null);
|
||||
isAgentDraftResultAutoOpenSuppressedRef.current = false;
|
||||
invalidateAgentSessionSyncRequests();
|
||||
setAgentSession(null);
|
||||
setAgentOperation(null);
|
||||
setStreamingAgentReplyText('');
|
||||
setIsStreamingAgentReply(false);
|
||||
setPendingAgentUserMessage(null);
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldError(null);
|
||||
setAgentDraftGenerationStartedAt(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setCustomWorldResultViewSource(null);
|
||||
|
||||
try {
|
||||
const { session } = await createRpgCreationSession(
|
||||
@@ -395,6 +483,7 @@ export function useRpgCreationSessionController(
|
||||
},
|
||||
[
|
||||
enterCreateTab,
|
||||
invalidateAgentSessionSyncRequests,
|
||||
isCreatingAgentSession,
|
||||
onSessionOpened,
|
||||
persistAgentUiState,
|
||||
@@ -404,7 +493,7 @@ export function useRpgCreationSessionController(
|
||||
|
||||
const submitAgentMessage = useCallback(
|
||||
async (payload: SendCustomWorldAgentMessageRequest) => {
|
||||
if (!activeAgentSessionId) {
|
||||
if (!activeAgentSessionId || isStreamingAgentReply) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -414,19 +503,18 @@ export function useRpgCreationSessionController(
|
||||
kind: 'chat',
|
||||
text: payload.text.trim(),
|
||||
});
|
||||
const pendingMessagePayload: PendingAgentUserMessage = {
|
||||
sessionId: activeAgentSessionId,
|
||||
message: optimisticUserMessage,
|
||||
};
|
||||
|
||||
setAgentOperation(null);
|
||||
persistAgentUiState(activeAgentSessionId, null);
|
||||
setStreamingAgentReplyText('');
|
||||
setIsStreamingAgentReply(true);
|
||||
setPendingAgentUserMessage(pendingMessagePayload);
|
||||
setAgentSession((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
messages: [...current.messages, optimisticUserMessage],
|
||||
updatedAt: optimisticUserMessage.createdAt,
|
||||
}
|
||||
: current,
|
||||
mergePendingAgentUserMessageIntoSession(current, pendingMessagePayload),
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -439,38 +527,58 @@ export function useRpgCreationSessionController(
|
||||
},
|
||||
},
|
||||
);
|
||||
setAgentSession(nextSession);
|
||||
const mergedNextSession = mergePendingAgentUserMessageIntoSession(
|
||||
nextSession,
|
||||
pendingMessagePayload,
|
||||
);
|
||||
setAgentSession(mergedNextSession);
|
||||
setAgentOperation(null);
|
||||
setStreamingAgentReplyText('');
|
||||
const hasServerEchoedPendingMessage = nextSession.messages.some(
|
||||
(message) => message.id === optimisticUserMessage.id,
|
||||
);
|
||||
setPendingAgentUserMessage(
|
||||
hasServerEchoedPendingMessage ? null : pendingMessagePayload,
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMessage = resolveRpgCreationErrorMessage(
|
||||
error,
|
||||
'发送共创消息失败。',
|
||||
);
|
||||
const warningMessage = buildOptimisticAgentMessage({
|
||||
id: `message-error-${Date.now()}`,
|
||||
role: 'assistant',
|
||||
kind: 'warning',
|
||||
text: errorMessage,
|
||||
});
|
||||
setAgentSession((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
messages: [
|
||||
...current.messages,
|
||||
buildOptimisticAgentMessage({
|
||||
id: `message-error-${Date.now()}`,
|
||||
role: 'assistant',
|
||||
kind: 'warning',
|
||||
text: errorMessage,
|
||||
}),
|
||||
],
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
: current,
|
||||
{
|
||||
const mergedCurrentSession = mergePendingAgentUserMessageIntoSession(
|
||||
current,
|
||||
pendingMessagePayload,
|
||||
);
|
||||
return mergedCurrentSession
|
||||
? {
|
||||
...mergedCurrentSession,
|
||||
messages: [...mergedCurrentSession.messages, warningMessage],
|
||||
updatedAt: warningMessage.createdAt,
|
||||
}
|
||||
: current;
|
||||
},
|
||||
);
|
||||
setPendingAgentUserMessage(null);
|
||||
setStreamingAgentReplyText('');
|
||||
persistAgentUiState(activeAgentSessionId, null);
|
||||
} finally {
|
||||
setIsStreamingAgentReply(false);
|
||||
}
|
||||
},
|
||||
[activeAgentSessionId, persistAgentUiState],
|
||||
[
|
||||
activeAgentSessionId,
|
||||
isStreamingAgentReply,
|
||||
mergePendingAgentUserMessageIntoSession,
|
||||
persistAgentUiState,
|
||||
],
|
||||
);
|
||||
|
||||
const executeAgentAction = useCallback(
|
||||
@@ -532,6 +640,7 @@ export function useRpgCreationSessionController(
|
||||
setAgentOperation(null);
|
||||
setStreamingAgentReplyText('');
|
||||
setIsStreamingAgentReply(false);
|
||||
setPendingAgentUserMessage(null);
|
||||
setAgentDraftGenerationStartedAt(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setCustomWorldResultViewSource(null);
|
||||
|
||||
@@ -199,6 +199,11 @@ export function useRpgEntryLibraryDetail(
|
||||
const openSavedCustomWorldEditor = useCallback(
|
||||
(entry: CustomWorldLibraryEntry<CustomWorldProfile>) => {
|
||||
setSelectedDetailEntry(entry);
|
||||
resetAutoSaveTrackingToIdle();
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setCustomWorldResultViewSource(null);
|
||||
setCustomWorldError(null);
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
const normalizedProfile = normalizeRpgEntryAgentBackedProfile(
|
||||
entry.profile,
|
||||
);
|
||||
@@ -230,7 +235,9 @@ export function useRpgEntryLibraryDetail(
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldAutoSaveError(null);
|
||||
setCustomWorldAutoSaveState('idle');
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setCustomWorldResultViewSource(null);
|
||||
resetAutoSaveTrackingToIdle();
|
||||
|
||||
const shouldOpenAgentWorkspace =
|
||||
|
||||
Reference in New Issue
Block a user