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,119 @@
import { ArrowRight } from 'lucide-react';
import { UnifiedModal } from '../common/UnifiedModal';
import { PLATFORM_CREATION_TYPES } from './platformEntryCreationTypes';
export interface PlatformEntryCreationTypeModalProps {
isOpen: boolean;
isBusy: boolean;
error: string | null;
onClose: () => void;
onSelectRpg: () => void;
onSelectBigFish: () => void;
onSelectPuzzle: () => void;
}
function CreationTypeCard(props: {
item: (typeof PLATFORM_CREATION_TYPES)[number];
busy: boolean;
onSelect: () => void;
}) {
const { item, busy, onSelect } = props;
const disabled = item.locked || busy;
return (
<button
type="button"
disabled={disabled}
onClick={onSelect}
className={`platform-interactive-card relative overflow-hidden rounded-[1.65rem] border px-4 py-4 text-left ${
item.locked
? 'cursor-not-allowed border-[var(--platform-subpanel-border)] bg-[var(--platform-subpanel-fill)] text-[var(--platform-text-soft)]'
: 'border-[var(--platform-cool-border)] bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.24),transparent_34%),linear-gradient(135deg,rgba(255,79,139,0.96),rgba(255,145,110,0.9))] text-white'
} ${busy && !item.locked ? 'opacity-70' : ''}`}
>
<div className="flex items-start justify-between gap-3">
<span
className={`platform-pill px-3 ${
item.locked
? 'platform-pill--neutral text-[var(--platform-text-soft)]'
: 'platform-pill--neutral border-white/30 bg-white/18 text-white'
}`}
>
{item.locked ? item.badge : busy ? '正在开启' : item.badge}
</span>
{item.locked ? (
<span className="text-lg leading-none text-white/45">·</span>
) : (
<ArrowRight className="h-4 w-4 text-white/80" />
)}
</div>
<div className="mt-8 text-xl font-black leading-tight text-inherit">
{item.title}
</div>
<div
className={`mt-2 text-sm ${
item.locked ? 'text-zinc-500' : 'text-zinc-200/82'
}`}
>
{item.subtitle}
</div>
</button>
);
}
/**
* 平台入口创作类型弹层。
* 多玩法入口统一在这里分流,避免把非 RPG 玩法写进 RPG 命名脚本。
*/
export function PlatformEntryCreationTypeModal({
isOpen,
isBusy,
error,
onClose,
onSelectRpg,
onSelectBigFish,
onSelectPuzzle,
}: PlatformEntryCreationTypeModalProps) {
if (!isOpen) {
return null;
}
return (
<UnifiedModal
open={isOpen}
title="选择创作类型"
description="先选玩法类型,再进入对应创作工作台。"
onClose={onClose}
closeDisabled={isBusy}
size="lg"
>
<div className="grid gap-3 sm:grid-cols-5">
{PLATFORM_CREATION_TYPES.map((item) => (
<CreationTypeCard
key={item.id}
item={item}
busy={isBusy}
onSelect={() => {
if (item.id === 'rpg') {
onSelectRpg();
}
if (item.id === 'big-fish') {
onSelectBigFish();
}
if (item.id === 'puzzle') {
onSelectPuzzle();
}
}}
/>
))}
</div>
{error ? (
<div className="platform-banner platform-banner--danger mt-4 rounded-[1.25rem] text-sm leading-6">
{error}
</div>
) : null}
</UnifiedModal>
);
}

View File

@@ -0,0 +1,17 @@
import { PlatformEntryFlowShellImpl } from './PlatformEntryFlowShellImpl';
import type {
PlatformEntryFlowShellProps,
SelectionStage,
} from './platformEntryTypes';
export type { PlatformEntryFlowShellProps, SelectionStage };
/**
* 平台入口通用壳层。
* RPG、Big Fish 等玩法创作入口在这里并列分流。
*/
export function PlatformEntryFlowShell(props: PlatformEntryFlowShellProps) {
return <PlatformEntryFlowShellImpl {...props} />;
}
export default PlatformEntryFlowShell;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
/**
* 平台首页视图的通用出口。
* 当前先复用成熟的首页实现,但避免让 `platform-entry` 继续直接依赖 RPG 命名组件。
*/
export {
RpgEntryHomeView as PlatformEntryHomeView,
type RpgEntryHomeViewProps as PlatformEntryHomeViewProps,
type PlatformHomeTab,
} from '../rpg-entry/RpgEntryHomeView';

View File

@@ -0,0 +1,8 @@
/**
* 平台作品详情视图的通用出口。
* 这里保留平台语义封装,减少 Big Fish 继续挂在 RPG 命名路径上的误导。
*/
export {
RpgEntryWorldDetailView as PlatformEntryWorldDetailView,
type RpgEntryWorldDetailViewProps as PlatformEntryWorldDetailViewProps,
} from '../rpg-entry/RpgEntryWorldDetailView';

View File

@@ -0,0 +1,9 @@
export {
PlatformEntryFlowShell,
type PlatformEntryFlowShellProps,
type SelectionStage,
} from './PlatformEntryFlowShell';
export {
PlatformEntryCreationTypeModal,
type PlatformEntryCreationTypeModalProps,
} from './PlatformEntryCreationTypeModal';

View File

@@ -0,0 +1,55 @@
export type PlatformCreationTypeId =
| 'rpg'
| 'big-fish'
| 'puzzle'
| 'airp'
| 'visual-novel';
export type PlatformCreationTypeCard = {
id: PlatformCreationTypeId;
title: string;
subtitle: string;
badge: string;
locked: boolean;
};
/**
* 创作页与类型弹层共用同一份模板元数据,避免多入口文案和可用状态漂移。
*/
export const PLATFORM_CREATION_TYPES: PlatformCreationTypeCard[] = [
{
id: 'rpg',
title: '角色扮演 RPG',
subtitle: 'Agent 共创',
badge: '可创建',
locked: false,
},
{
id: 'big-fish',
title: '大鱼吃小鱼',
subtitle: '实时成长玩法',
badge: '可创建',
locked: false,
},
{
id: 'puzzle',
title: '拼图玩法',
subtitle: '图像锚点共创',
badge: '可创建',
locked: false,
},
{
id: 'airp',
title: 'AIRP',
subtitle: '敬请期待',
badge: '锁定',
locked: true,
},
{
id: 'visual-novel',
title: '视觉小说',
subtitle: '敬请期待',
badge: '锁定',
locked: true,
},
];

View File

@@ -0,0 +1,9 @@
/**
* 平台入口共享 helper 的通用封装层。
* 先复用既有实现,同时把多玩法入口依赖从 RPG 命名中隔离出来。
*/
export {
buildCreationHubFallbackItems,
normalizeAgentBackedProfile,
resolveRpgCreationErrorMessage,
} from '../rpg-entry/rpgEntryShared';

View File

@@ -0,0 +1,42 @@
import type {
CustomWorldAgentSessionSnapshot,
} from '../../../packages/shared/src/contracts/customWorldAgent';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import type { CustomWorldProfile } from '../../types';
export type SelectionStage =
| 'platform'
| 'detail'
| 'agent-workspace'
| 'big-fish-agent-workspace'
| 'big-fish-generating'
| 'big-fish-result'
| 'big-fish-runtime'
| 'puzzle-agent-workspace'
| 'puzzle-generating'
| 'puzzle-result'
| 'puzzle-gallery-detail'
| 'puzzle-runtime'
| 'custom-world-generating'
| 'custom-world-result';
export type CustomWorldGenerationViewSource = 'agent-draft-foundation' | null;
export type CustomWorldResultViewSource = 'saved-profile' | 'agent-draft' | null;
export type CustomWorldAutoSaveState = 'idle' | 'saving' | 'saved' | 'error';
export type SyncedAgentDraftResult = {
session: CustomWorldAgentSessionSnapshot | null;
profile: CustomWorldProfile | null;
};
export type PlatformEntryFlowShellProps = {
selectionStage: SelectionStage;
setSelectionStage: (stage: SelectionStage) => void;
hasSavedGame: boolean;
savedSnapshot: HydratedSavedGameSnapshot | null;
handleContinueGame: (snapshot?: HydratedSavedGameSnapshot | null) => void;
handleStartNewGame: () => void;
handleCustomWorldSelect: (customWorldProfile: CustomWorldProfile) => void;
};

View File

@@ -0,0 +1,311 @@
import { useCallback, useState } from 'react';
import type { TextStreamOptions } from '../../services/aiTypes';
import type { SelectionStage } from './platformEntryTypes';
type CreationAgentMessageLike = {
clientMessageId: string;
text: string;
};
type CreationAgentSessionLike = {
sessionId: string;
draft?: unknown;
messages: Array<{
id: string;
role: string;
kind?: string;
text: string;
createdAt?: string;
}>;
updatedAt?: string;
};
type CreationAgentClientAdapter<
TSession extends CreationAgentSessionLike,
TCreatePayload,
TCreateResponse,
TMessagePayload extends CreationAgentMessageLike,
TActionPayload,
TActionResponse,
> = {
createSession: (payload: TCreatePayload) => Promise<TCreateResponse>;
getSession: (sessionId: string) => Promise<TCreateResponse>;
streamMessage: (
sessionId: string,
payload: TMessagePayload,
options?: TextStreamOptions,
) => Promise<TSession>;
executeAction: (
sessionId: string,
payload: TActionPayload,
) => Promise<TActionResponse>;
selectSession: (response: TCreateResponse) => TSession;
};
type PlatformCreationAgentFlowControllerOptions<
TSession extends CreationAgentSessionLike,
TCreatePayload,
TCreateResponse,
TMessagePayload extends CreationAgentMessageLike,
TActionPayload,
TActionResponse,
> = {
client: CreationAgentClientAdapter<
TSession,
TCreatePayload,
TCreateResponse,
TMessagePayload,
TActionPayload,
TActionResponse
>;
createPayload: TCreatePayload;
workspaceStage: SelectionStage;
resultStage: SelectionStage;
platformStage: SelectionStage;
isCompileAction: (payload: TActionPayload) => boolean;
resolveErrorMessage: (error: unknown, fallback: string) => string;
errorMessages: {
open: string;
restoreMissingSession: string;
restore: string;
submit: string;
execute: string;
};
enterCreateTab: () => void;
setSelectionStage: (stage: SelectionStage) => void;
onSessionOpened?: () => void;
onActionComplete?: (params: {
payload: TActionPayload;
response: TActionResponse;
session: TSession;
setSession: (session: TSession) => void;
}) => Promise<void> | void;
beforeExecuteAction?: (params: {
payload: TActionPayload;
session: TSession;
}) => void;
onActionError?: (params: {
payload: TActionPayload;
error: unknown;
errorMessage: string;
}) => void;
};
function buildOptimisticMessage<TMessagePayload extends CreationAgentMessageLike>(
payload: TMessagePayload,
) {
return {
id: payload.clientMessageId,
role: 'user',
kind: 'chat',
text: payload.text.trim(),
createdAt: new Date().toISOString(),
};
}
/**
* 轻量作品 Agent 创作流程的通用前端控制器。
* 这里只处理跨玩法一致的会话、流式消息、忙碌态与草稿恢复,玩法结果页和运行态动作留给外层。
*/
export function usePlatformCreationAgentFlowController<
TSession extends CreationAgentSessionLike,
TCreatePayload,
TCreateResponse,
TMessagePayload extends CreationAgentMessageLike,
TActionPayload,
TActionResponse,
>(
options: PlatformCreationAgentFlowControllerOptions<
TSession,
TCreatePayload,
TCreateResponse,
TMessagePayload,
TActionPayload,
TActionResponse
>,
) {
const [session, setSession] = useState<TSession | null>(null);
const [error, setError] = useState<string | null>(null);
const [isBusy, setIsBusy] = useState(false);
const [streamingReplyText, setStreamingReplyText] = useState('');
const [isStreamingReply, setIsStreamingReply] = useState(false);
const openWorkspace = useCallback(async () => {
if (isBusy) {
return;
}
setIsBusy(true);
setError(null);
setStreamingReplyText('');
setIsStreamingReply(false);
try {
const response = await options.client.createSession(options.createPayload);
setSession(options.client.selectSession(response));
options.enterCreateTab();
options.onSessionOpened?.();
options.setSelectionStage(options.workspaceStage);
} catch (caughtError) {
setError(
options.resolveErrorMessage(caughtError, options.errorMessages.open),
);
} finally {
setIsBusy(false);
}
}, [isBusy, options]);
const restoreDraft = useCallback(
async (sessionId: string | null | undefined) => {
const normalizedSessionId = sessionId?.trim();
if (!normalizedSessionId) {
setError(options.errorMessages.restoreMissingSession);
return null;
}
setIsBusy(true);
setError(null);
setStreamingReplyText('');
setIsStreamingReply(false);
try {
const response = await options.client.getSession(normalizedSessionId);
const nextSession = options.client.selectSession(response);
setSession(nextSession);
options.enterCreateTab();
options.setSelectionStage(
nextSession.draft ? options.resultStage : options.workspaceStage,
);
return nextSession;
} catch (caughtError) {
setError(
options.resolveErrorMessage(caughtError, options.errorMessages.restore),
);
options.enterCreateTab();
options.setSelectionStage(options.platformStage);
return null;
} finally {
setIsBusy(false);
}
},
[options],
);
const submitMessage = useCallback(
async (payload: TMessagePayload) => {
if (!session || isStreamingReply) {
return;
}
const optimisticMessage = buildOptimisticMessage(payload);
setError(null);
setStreamingReplyText('');
setIsStreamingReply(true);
setSession((current) =>
current
? {
...current,
messages: [...current.messages, optimisticMessage],
updatedAt: optimisticMessage.createdAt,
}
: current,
);
try {
const nextSession = await options.client.streamMessage(
session.sessionId,
payload,
{
onUpdate: setStreamingReplyText,
},
);
setSession(nextSession);
setStreamingReplyText('');
} catch (caughtError) {
setError(
options.resolveErrorMessage(caughtError, options.errorMessages.submit),
);
} finally {
setIsStreamingReply(false);
}
},
[isStreamingReply, options, session],
);
const executeAction = useCallback(
async (payload: TActionPayload) => {
if (!session || isBusy) {
return;
}
setIsBusy(true);
setError(null);
try {
options.beforeExecuteAction?.({ payload, session });
const response = await options.client.executeAction(
session.sessionId,
payload,
);
await options.onActionComplete?.({
payload,
response,
session,
setSession,
});
if (options.isCompileAction(payload)) {
options.setSelectionStage(options.resultStage);
}
} catch (caughtError) {
const errorMessage = options.resolveErrorMessage(
caughtError,
options.errorMessages.execute,
);
setError(errorMessage);
options.onActionError?.({
payload,
error: caughtError,
errorMessage,
});
} finally {
setIsBusy(false);
}
},
[isBusy, options, session],
);
const leaveFlow = useCallback(() => {
setError(null);
setStreamingReplyText('');
setIsStreamingReply(false);
options.enterCreateTab();
options.setSelectionStage(options.platformStage);
}, [options]);
const resetTransientState = useCallback(() => {
setError(null);
setStreamingReplyText('');
setIsStreamingReply(false);
}, []);
return {
session,
setSession,
error,
setError,
isBusy,
setIsBusy,
streamingReplyText,
setStreamingReplyText,
isStreamingReply,
setIsStreamingReply,
openWorkspace,
restoreDraft,
submitMessage,
executeAction,
leaveFlow,
resetTransientState,
};
}

View File

@@ -0,0 +1,5 @@
/**
* 平台入口 bootstrap 通用封装。
* 现阶段逻辑仍复用既有实现,但对外暴露平台语义命名。
*/
export { useRpgEntryBootstrap as usePlatformEntryBootstrap } from '../rpg-entry/useRpgEntryBootstrap';

View File

@@ -0,0 +1,5 @@
/**
* 平台入口详情态编排通用封装。
* 通过平台语义出口避免 Big Fish 直接依赖 RPG 命名 hook。
*/
export { useRpgEntryLibraryDetail as usePlatformEntryLibraryDetail } from '../rpg-entry/useRpgEntryLibraryDetail';

View File

@@ -0,0 +1,5 @@
/**
* 平台入口导航通用封装。
* 多玩法统一从 `platform-entry` 暴露RPG 目录只保留兼容与 RPG 专属能力。
*/
export { useRpgEntryNavigation as usePlatformEntryNavigation } from '../rpg-entry/useRpgEntryNavigation';