init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
/* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, expect, test, vi } from 'vitest';
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
import { CustomWorldAgentWorkspace } from './CustomWorldAgentWorkspace';
const baseSession: CustomWorldAgentSessionSnapshot = {
sessionId: 'custom-world-agent-session-1',
currentTurn: 4,
anchorContent: {
worldPromise:
'一个被潮雾改写航线秩序的群岛世界,所有通路都要向未知代价借路,体验压迫、潮湿、悬疑。',
playerFantasy:
'玩家是被迫返乡的旧航路继承人,目标是查清沉船夜背后的真相,失败会再次失去唯一还活着的旧友。',
themeBoundary: null,
playerEntryPoint: null,
coreConflict: null,
keyRelationships: null,
hiddenLines: null,
iconicElements: null,
},
progressPercent: 58,
lastAssistantReply: '世界和玩家视角已经有了,下一步我想把最明面的冲突钉住。',
stage: 'collecting_intent',
focusCardId: null,
creatorIntent: {},
creatorIntentReadiness: {
isReady: false,
completedKeys: ['world_hook', 'player_premise'],
missingKeys: ['theme_and_tone', 'core_conflict', 'relationship_seed', 'iconic_element'],
},
anchorPack: {},
lockState: {},
draftProfile: null,
messages: [
{
id: 'message-1',
role: 'assistant',
kind: 'chat',
text: '先告诉我你想做一个怎样的世界。',
createdAt: '2026-04-17T12:00:00.000Z',
relatedOperationId: null,
},
],
draftCards: [],
pendingClarifications: [],
suggestedActions: [],
recommendedReplies: [],
qualityFindings: [],
assetCoverage: {
roleAssets: [],
sceneAssets: [],
allRoleAssetsReady: false,
allSceneAssetsReady: false,
},
updatedAt: '2026-04-17T12:00:00.000Z',
};
beforeEach(() => {
if (!Element.prototype.scrollIntoView) {
Element.prototype.scrollIntoView = () => {};
}
});
test('workspace sends summary request from progress area', async () => {
const user = userEvent.setup();
const onSubmitMessage = vi.fn();
render(
<CustomWorldAgentWorkspace
session={baseSession}
activeOperation={null}
onBack={() => {}}
onSubmitMessage={onSubmitMessage}
onExecuteAction={() => {}}
/>,
);
await user.click(screen.getByRole('button', { name: '总结当前设定' }));
expect(onSubmitMessage).toHaveBeenCalledWith(
expect.objectContaining({
text: '请总结一下当前已经成形的世界设定。',
}),
);
});
test('workspace enables quick fill after at least two turns and submits quick fill request', async () => {
const user = userEvent.setup();
const onSubmitMessage = vi.fn();
render(
<CustomWorldAgentWorkspace
session={baseSession}
activeOperation={null}
onBack={() => {}}
onSubmitMessage={onSubmitMessage}
onExecuteAction={() => {}}
/>,
);
await user.click(screen.getByRole('button', { name: '补充剩余设定' }));
expect(onSubmitMessage).toHaveBeenCalledWith(
expect.objectContaining({
text: '请补充剩余设定。',
quickFillRequested: true,
}),
);
});
test('workspace hides quick fill before two turns', () => {
render(
<CustomWorldAgentWorkspace
session={{
...baseSession,
currentTurn: 1,
}}
activeOperation={null}
onBack={() => {}}
onSubmitMessage={() => {}}
onExecuteAction={() => {}}
/>,
);
expect(screen.queryByRole('button', { name: '补全剩余设定' })).toBeNull();
});
test('workspace exposes draft action when progress reaches 100', async () => {
const user = userEvent.setup();
const onExecuteAction = vi.fn();
render(
<CustomWorldAgentWorkspace
session={{
...baseSession,
progressPercent: 100,
stage: 'foundation_review',
}}
activeOperation={null}
onBack={() => {}}
onSubmitMessage={() => {}}
onExecuteAction={onExecuteAction}
/>,
);
await user.click(screen.getByRole('button', { name: '生成游戏设定草稿' }));
expect(onExecuteAction).toHaveBeenCalledWith({
action: 'draft_foundation',
});
});
test('workspace hides draft action before progress reaches 100', () => {
render(
<CustomWorldAgentWorkspace
session={{
...baseSession,
progressPercent: 99,
}}
activeOperation={null}
onBack={() => {}}
onSubmitMessage={() => {}}
onExecuteAction={() => {}}
/>,
);
expect(
screen.queryByRole('button', { name: '生成游戏设定草稿' }),
).toBeNull();
});
test('workspace submits recommended reply from thread', async () => {
const user = userEvent.setup();
const onSubmitMessage = vi.fn();
render(
<CustomWorldAgentWorkspace
session={{
...baseSession,
recommendedReplies: ['继续补充这个世界的核心冲突'],
}}
activeOperation={null}
onBack={() => {}}
onSubmitMessage={onSubmitMessage}
onExecuteAction={() => {}}
/>,
);
await user.click(
screen.getByRole('button', { name: '继续补充这个世界的核心冲突' }),
);
expect(onSubmitMessage).toHaveBeenCalledWith(
expect.objectContaining({
text: '继续补充这个世界的核心冲突',
quickFillRequested: false,
focusCardId: null,
selectedCardIds: [],
}),
);
});

View File

@@ -0,0 +1,87 @@
import { renderToStaticMarkup } from 'react-dom/server';
import { expect, test } from 'vitest';
import { CustomWorldAgentWorkspace } from './CustomWorldAgentWorkspace';
test('custom world agent workspace renders minimum loop chat layout', () => {
const html = renderToStaticMarkup(
<CustomWorldAgentWorkspace
session={{
sessionId: 'custom-world-agent-session-1',
currentTurn: 3,
anchorContent: {
worldPromise:
'一个被潮雾改写航线秩序的群岛世界,所有人都要为每一次借路付出代价,体验压迫、悬疑、带一点海上传奇感。',
playerFantasy: null,
themeBoundary: null,
playerEntryPoint: null,
coreConflict: null,
keyRelationships: null,
hiddenLines: null,
iconicElements: null,
},
progressPercent: 42,
lastAssistantReply: '我先把世界底色收住了,接下来想确认玩家会怎么被卷进来。',
stage: 'collecting_intent',
focusCardId: null,
creatorIntent: {},
creatorIntentReadiness: {
isReady: false,
completedKeys: ['world_hook'],
missingKeys: [
'player_premise',
'theme_and_tone',
'core_conflict',
'relationship_seed',
'iconic_element',
],
},
anchorPack: {},
lockState: {},
draftProfile: null,
messages: [
{
id: 'message-1',
role: 'assistant',
kind: 'chat',
text: '先告诉我你想做一个怎样的世界。',
createdAt: new Date().toISOString(),
relatedOperationId: null,
},
],
draftCards: [],
pendingClarifications: [],
suggestedActions: [],
recommendedReplies: [],
qualityFindings: [],
assetCoverage: {
roleAssets: [],
sceneAssets: [],
allRoleAssetsReady: false,
allSceneAssetsReady: false,
},
updatedAt: new Date().toISOString(),
}}
activeOperation={null}
onBack={() => {}}
onSubmitMessage={() => {}}
onExecuteAction={() => {}}
/>,
);
expect(html).toContain('创作进度');
expect(html).toContain('42%');
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('当前轮次');
expect(html).not.toContain('当前状态');
expect(html).not.toContain('草稿抽屉');
expect(html).not.toContain('快捷动作');
});

View File

@@ -0,0 +1,218 @@
import type {
CustomWorldAgentActionRequest,
CustomWorldAgentOperationRecord,
CustomWorldAgentSessionSnapshot,
SendCustomWorldAgentMessageRequest,
} from '../../../packages/shared/src/contracts/customWorldAgent';
import {
buildCreationAgentChatMessage,
createCreationAgentChatQuickActions,
createCreationAgentClientMessageId,
isCreationAgentOperationBusy,
resolveCreationAgentQuickActionMessage,
} from '../../services/creation-agent';
import {
type CreationAgentAnchorView,
type CreationAgentOperationView,
type CreationAgentSessionView,
type CreationAgentTheme,
CreationAgentWorkspace,
} from '../creation-agent';
type CustomWorldAgentWorkspaceProps = {
session: CustomWorldAgentSessionSnapshot | null;
activeOperation: CustomWorldAgentOperationRecord | null;
streamingReplyText?: string;
isStreamingReply?: boolean;
onBack: () => void;
onSubmitMessage: (payload: SendCustomWorldAgentMessageRequest) => void;
onExecuteAction: (payload: CustomWorldAgentActionRequest) => void;
};
const CUSTOM_WORLD_AGENT_THEME: CreationAgentTheme = {
accentTextClass: 'text-emerald-100/86',
accentBgClass: 'bg-emerald-300',
accentButtonClass: 'bg-emerald-200 shadow-emerald-950/20',
userBubbleClass:
'border border-[var(--platform-cool-border)] bg-[var(--platform-cool-bg)] text-[var(--platform-text-strong)]',
heroClass:
'border border-emerald-100/18 bg-[radial-gradient(circle_at_top_left,rgba(52,211,153,0.2),transparent_32%),linear-gradient(135deg,rgba(6,78,59,0.95),rgba(24,33,39,0.96))]',
anchorGridClass: 'grid gap-2 sm:grid-cols-2 xl:grid-cols-4',
};
function stringifyAnchorValue(value: unknown): string {
if (!value) {
return '';
}
if (typeof value === 'string') {
return value;
}
if (Array.isArray(value)) {
return value
.map((item): string => stringifyAnchorValue(item))
.filter((item): item is string => Boolean(item))
.join(' / ');
}
if (typeof value !== 'object') {
return String(value);
}
return Object.values(value as Record<string, unknown>)
.map((item): string => stringifyAnchorValue(item))
.filter((item): item is string => Boolean(item))
.join(' / ');
}
function buildCustomWorldAnchor(
key: string,
label: string,
value: unknown,
): CreationAgentAnchorView {
const text = stringifyAnchorValue(value);
return {
key,
label,
value: text,
status: text ? 'confirmed' : 'missing',
};
}
function mapCustomWorldSession(
session: CustomWorldAgentSessionSnapshot,
): CreationAgentSessionView {
return {
sessionId: session.sessionId,
// 自定义世界 Agent 聊天页顶部只保留操作与进度,不展示标题和引导副文案。
title: null,
assistantSummary: null,
currentTurn: session.currentTurn,
progressPercent: session.progressPercent,
anchors: [
buildCustomWorldAnchor(
'worldPromise',
'世界承诺',
session.anchorContent.worldPromise,
),
buildCustomWorldAnchor(
'playerFantasy',
'玩家幻想',
session.anchorContent.playerFantasy,
),
buildCustomWorldAnchor(
'themeBoundary',
'主题边界',
session.anchorContent.themeBoundary,
),
buildCustomWorldAnchor(
'playerEntryPoint',
'开局切入',
session.anchorContent.playerEntryPoint,
),
buildCustomWorldAnchor(
'coreConflict',
'核心冲突',
session.anchorContent.coreConflict,
),
buildCustomWorldAnchor(
'keyRelationships',
'关键关系',
session.anchorContent.keyRelationships,
),
buildCustomWorldAnchor(
'hiddenLines',
'暗线',
session.anchorContent.hiddenLines,
),
buildCustomWorldAnchor(
'iconicElements',
'标志元素',
session.anchorContent.iconicElements,
),
],
messages: session.messages,
recommendedReplies: session.recommendedReplies,
};
}
function mapCustomWorldOperation(
operation: CustomWorldAgentOperationRecord | null,
): CreationAgentOperationView | null {
if (!operation || operation.type === 'process_message') {
return null;
}
return {
operationId: operation.operationId,
type: operation.type,
status: operation.status,
phaseLabel: operation.phaseLabel,
phaseDetail: operation.phaseDetail,
progress: operation.progress,
error: operation.error,
};
}
export function CustomWorldAgentWorkspace({
session,
activeOperation,
streamingReplyText = '',
isStreamingReply = false,
onBack,
onSubmitMessage,
onExecuteAction,
}: CustomWorldAgentWorkspaceProps) {
const isBusy =
isCreationAgentOperationBusy(activeOperation) || isStreamingReply;
const submitMessage = (text: string, quickFillRequested = false) => {
onSubmitMessage(
buildCreationAgentChatMessage({
clientMessageId: createCreationAgentClientMessageId('custom-world'),
text,
quickFillRequested,
extraPayload: {
focusCardId: null,
selectedCardIds: [],
},
}),
);
};
return (
<CreationAgentWorkspace
session={session ? mapCustomWorldSession(session) : null}
theme={CUSTOM_WORLD_AGENT_THEME}
loadingText="正在恢复"
composerPlaceholder="输入消息"
primaryActionLabel="生成游戏设定草稿"
activeOperation={mapCustomWorldOperation(activeOperation)}
streamingReplyText={streamingReplyText}
isStreamingReply={isStreamingReply}
isBusy={isBusy}
quickActions={createCreationAgentChatQuickActions()}
onBack={onBack}
onSubmitText={(text) => {
submitMessage(text);
}}
onPrimaryAction={() => {
onExecuteAction({
action: 'draft_foundation',
});
}}
onQuickAction={(action) => {
const quickActionMessage = resolveCreationAgentQuickActionMessage(
action.key,
'请总结一下当前已经成形的世界设定。',
);
submitMessage(
quickActionMessage.text,
quickActionMessage.quickFillRequested,
);
}}
/>
);
}