Files
Genarrative/src/components/platform-entry/usePlatformCreationAgentFlowController.ts
五香丸子 e8fee0172a
Some checks failed
CI / verify (push) Has been cancelled
feat: add puzzle onboarding and match3d entry updates
2026-05-07 23:30:54 +08:00

362 lines
9.7 KiB
TypeScript

import { useCallback, useRef, 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;
onOpenError?: (params: {
error: unknown;
errorMessage: string;
}) => 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(),
};
}
function buildInterruptedAssistantMessage(text: string) {
return {
id: `assistant-interrupted-${Date.now().toString(36)}`,
role: 'assistant',
kind: 'warning',
text: 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 latestStreamingReplyTextRef = useRef('');
const updateStreamingReplyText = useCallback((text: string) => {
latestStreamingReplyTextRef.current = text;
setStreamingReplyText(text);
}, []);
const resetStreamingReply = useCallback(() => {
latestStreamingReplyTextRef.current = '';
setStreamingReplyText('');
setIsStreamingReply(false);
}, []);
const openWorkspace = useCallback(async (createPayload?: TCreatePayload) => {
if (isBusy) {
return null;
}
setIsBusy(true);
setError(null);
resetStreamingReply();
try {
const response = await options.client.createSession(
createPayload ?? options.createPayload,
);
const nextSession = options.client.selectSession(response);
setSession(nextSession);
options.enterCreateTab();
options.onSessionOpened?.();
options.setSelectionStage(options.workspaceStage);
return nextSession;
} catch (caughtError) {
const errorMessage = options.resolveErrorMessage(
caughtError,
options.errorMessages.open,
);
setError(errorMessage);
options.onOpenError?.({
error: caughtError,
errorMessage,
});
return null;
} finally {
setIsBusy(false);
}
}, [isBusy, options, resetStreamingReply]);
const restoreDraft = useCallback(
async (sessionId: string | null | undefined) => {
const normalizedSessionId = sessionId?.trim();
if (!normalizedSessionId) {
setError(options.errorMessages.restoreMissingSession);
return null;
}
setIsBusy(true);
setError(null);
resetStreamingReply();
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, resetStreamingReply],
);
const submitMessage = useCallback(
async (payload: TMessagePayload) => {
if (!session || isStreamingReply) {
return;
}
const optimisticMessage = buildOptimisticMessage(payload);
setError(null);
updateStreamingReplyText('');
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: updateStreamingReplyText,
},
);
setSession(nextSession);
updateStreamingReplyText('');
} catch (caughtError) {
const interruptedReplyText =
latestStreamingReplyTextRef.current.trim();
// 上游流可能在已经吐出可读回复后才失败;把这段回复落进本地消息列表,避免 UI 收尾时突然消失。
if (interruptedReplyText) {
const interruptedMessage =
buildInterruptedAssistantMessage(interruptedReplyText);
setSession((current) =>
current
? {
...current,
messages: [...current.messages, interruptedMessage],
updatedAt: interruptedMessage.createdAt,
}
: current,
);
}
setError(
options.resolveErrorMessage(caughtError, options.errorMessages.submit),
);
} finally {
setIsStreamingReply(false);
}
},
[isStreamingReply, options, session, updateStreamingReplyText],
);
const executeAction = useCallback(
async (payload: TActionPayload, sessionOverride?: TSession | null) => {
const targetSession = sessionOverride ?? session;
if (!targetSession || isBusy) {
return;
}
setIsBusy(true);
setError(null);
try {
options.beforeExecuteAction?.({ payload, session: targetSession });
const response = await options.client.executeAction(
targetSession.sessionId,
payload,
);
await options.onActionComplete?.({
payload,
response,
session: targetSession,
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);
resetStreamingReply();
options.enterCreateTab();
options.setSelectionStage(options.platformStage);
}, [options, resetStreamingReply]);
const resetTransientState = useCallback(() => {
setError(null);
resetStreamingReply();
}, [resetStreamingReply]);
return {
session,
setSession,
error,
setError,
isBusy,
setIsBusy,
streamingReplyText,
setStreamingReplyText,
isStreamingReply,
setIsStreamingReply,
openWorkspace,
restoreDraft,
submitMessage,
executeAction,
leaveFlow,
resetTransientState,
};
}