feat(chat): sync multi-role backend APIs

This commit is contained in:
2026-07-20 11:29:54 +08:00
parent 16b5c16e76
commit b6fdc912ae
84 changed files with 1488 additions and 439 deletions
@@ -20,7 +20,7 @@ const defaultScope = {
};
const promotionSession: PendingChatPromotion = {
characterId: "character_elio",
characterId: "elio",
promotionType: "image",
lockType: "image_paywall",
clientLockId: "promotion-1",
@@ -182,7 +182,7 @@ function createPendingUnlock(
): PendingChatUnlock {
return {
reason: "single_message_unlock",
characterId: "character_elio",
characterId: "elio",
...input,
stage: "auth",
createdAt: 1,
+45 -1
View File
@@ -13,6 +13,9 @@ import {
useActiveCharacter,
useActiveCharacterRoutes,
} from "@/providers/character-provider";
import { useCharacterCatalog } from "@/providers/character-catalog-provider";
import { clearPendingChatNavigation } from "@/lib/navigation/chat_unlock_session";
import { getCharacterRoutes } from "@/router/routes";
import { MobileShell } from "@/app/_components/core";
@@ -50,6 +53,9 @@ const chatShellStyle = {
export function ChatScreen() {
const router = useRouter();
const character = useActiveCharacter();
const characterCatalog = useCharacterCatalog();
const refreshCharacterCatalog = characterCatalog.refresh;
const defaultCharacterSlug = characterCatalog.defaultCharacter.slug;
const characterRoutes = useActiveCharacterRoutes();
const searchParams = useSearchParams();
const state = useChatState();
@@ -133,6 +139,42 @@ export function ChatScreen() {
state.historyLoaded,
]);
useEffect(() => {
const errorCode = state.characterErrorCode;
if (errorCode === "CHARACTER_MISMATCH") {
chatDispatch({ type: "ChatHistoryRefreshRequested" });
return;
}
if (
errorCode !== "CHARACTER_DISABLED" &&
errorCode !== "CHARACTER_NOT_FOUND"
) {
return;
}
let cancelled = false;
void (async () => {
if (errorCode === "CHARACTER_NOT_FOUND") {
await clearPendingChatNavigation();
}
await refreshCharacterCatalog();
if (!cancelled) {
router.replace(
getCharacterRoutes(defaultCharacterSlug).splash,
);
}
})();
return () => {
cancelled = true;
};
}, [
chatDispatch,
defaultCharacterSlug,
refreshCharacterCatalog,
router,
state.characterErrorCode,
]);
function handleUnlockPrivateMessage(messageId: string): void {
unlockCoordinator.requestMessageUnlock(messageId, "private");
}
@@ -223,7 +265,9 @@ export function ChatScreen() {
onUnlock={messageLimitBanner.unlock}
/>
) : (
<ChatInputBar disabled={!state.historyLoaded} />
<ChatInputBar
disabled={!state.historyLoaded || !state.canSendMessage}
/>
)}
</div>
@@ -235,7 +235,7 @@ function renderChatArea(
act(() => {
root.render(
<ChatArea
characterId="character_elio"
characterId="elio"
messages={messages}
isReplyingAI={false}
initialScrollReady={initialScrollReady}
@@ -127,7 +127,7 @@ describe("chat Tailwind components", () => {
it("renders ImageBubble openable and paywalled states", () => {
const openableHtml = renderToStaticMarkup(
<ImageBubble
characterId="character_elio"
characterId="elio"
messageId="message-1"
imageUrl="/chat-image.png"
onOpenImage={() => undefined}
@@ -135,7 +135,7 @@ describe("chat Tailwind components", () => {
);
const paywalledHtml = renderToStaticMarkup(
<ImageBubble
characterId="character_elio"
characterId="elio"
imageUrl="/locked-image.png"
imagePaywalled
/>,
@@ -73,6 +73,7 @@ export const ChatInputTextField = forwardRef<
onBlur={() => onFocusChange?.(false)}
placeholder={placeholder}
disabled={disabled}
maxLength={4000}
autoFocus={autoFocus}
rows={1}
aria-label="Message"
-9
View File
@@ -1,9 +0,0 @@
import type { ReactNode } from "react";
export default function LegacyChatLayout({
children,
}: {
children: ReactNode;
}) {
return children;
}