refactor(characters): drive shared UI from active character
This commit is contained in:
@@ -9,7 +9,10 @@ import {
|
||||
useChatDispatch,
|
||||
useChatState,
|
||||
} from "@/stores/chat/chat-context";
|
||||
import { useActiveCharacterRoutes } from "@/providers/character-provider";
|
||||
import {
|
||||
useActiveCharacter,
|
||||
useActiveCharacterRoutes,
|
||||
} from "@/providers/character-provider";
|
||||
|
||||
import { MobileShell } from "@/app/_components/core";
|
||||
|
||||
@@ -46,6 +49,7 @@ const chatShellStyle = {
|
||||
|
||||
export function ChatScreen() {
|
||||
const router = useRouter();
|
||||
const character = useActiveCharacter();
|
||||
const characterRoutes = useActiveCharacterRoutes();
|
||||
const searchParams = useSearchParams();
|
||||
const state = useChatState();
|
||||
@@ -167,7 +171,7 @@ export function ChatScreen() {
|
||||
>
|
||||
<div className="pointer-events-none absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/images/chat/bg-chatpage.png"
|
||||
src={character.assets.chatBackground}
|
||||
alt=""
|
||||
fill
|
||||
priority
|
||||
|
||||
@@ -235,6 +235,7 @@ function renderChatArea(
|
||||
act(() => {
|
||||
root.render(
|
||||
<ChatArea
|
||||
characterId="character_elio"
|
||||
messages={messages}
|
||||
isReplyingAI={false}
|
||||
initialScrollReady={initialScrollReady}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getCharacterBySlug } from "@/data/constants/character";
|
||||
import { CharacterProvider } from "@/providers/character-provider";
|
||||
|
||||
import { LockedImageMessageCard } from "../locked-image-message-card";
|
||||
|
||||
describe("LockedImageMessageCard", () => {
|
||||
it("renders the promotion hint and an enabled unlock action", () => {
|
||||
const character = getCharacterBySlug("maya");
|
||||
if (!character) throw new Error("Missing Maya character fixture");
|
||||
const html = renderToStaticMarkup(
|
||||
<LockedImageMessageCard
|
||||
hint="Unlock this private photo."
|
||||
onUnlock={() => undefined}
|
||||
/>,
|
||||
<CharacterProvider character={character}>
|
||||
<LockedImageMessageCard
|
||||
hint="Unlock this private photo."
|
||||
onUnlock={() => undefined}
|
||||
/>
|
||||
</CharacterProvider>,
|
||||
);
|
||||
|
||||
expect(html).toContain('aria-label="Locked private image"');
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -25,8 +26,8 @@ vi.mock("@/router/use-app-navigator", () => ({
|
||||
|
||||
describe("chat Tailwind components", () => {
|
||||
it("renders MessageAvatar AI and user branches with image utilities", () => {
|
||||
const aiHtml = renderToStaticMarkup(<MessageAvatar isFromAI={true} />);
|
||||
const userHtml = renderToStaticMarkup(
|
||||
const aiHtml = renderWithCharacter(<MessageAvatar isFromAI={true} />);
|
||||
const userHtml = renderWithCharacter(
|
||||
<MessageAvatar isFromAI={false} userAvatarUrl="/user-avatar.png" />,
|
||||
);
|
||||
|
||||
@@ -71,8 +72,8 @@ describe("chat Tailwind components", () => {
|
||||
});
|
||||
|
||||
it("renders PrivateMessageCard with fallback and loading states", () => {
|
||||
const fallbackHtml = renderToStaticMarkup(<PrivateMessageCard />);
|
||||
const unlockingHtml = renderToStaticMarkup(
|
||||
const fallbackHtml = renderWithCharacter(<PrivateMessageCard />);
|
||||
const unlockingHtml = renderWithCharacter(
|
||||
<PrivateMessageCard hint="Premium secret" isUnlocking onUnlock={() => undefined} />,
|
||||
);
|
||||
|
||||
@@ -126,13 +127,18 @@ describe("chat Tailwind components", () => {
|
||||
it("renders ImageBubble openable and paywalled states", () => {
|
||||
const openableHtml = renderToStaticMarkup(
|
||||
<ImageBubble
|
||||
characterId="character_elio"
|
||||
messageId="message-1"
|
||||
imageUrl="/chat-image.png"
|
||||
onOpenImage={() => undefined}
|
||||
/>,
|
||||
);
|
||||
const paywalledHtml = renderToStaticMarkup(
|
||||
<ImageBubble imageUrl="/locked-image.png" imagePaywalled />,
|
||||
<ImageBubble
|
||||
characterId="character_elio"
|
||||
imageUrl="/locked-image.png"
|
||||
imagePaywalled
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(openableHtml).toContain('role="button"');
|
||||
@@ -170,14 +176,14 @@ describe("chat Tailwind components", () => {
|
||||
});
|
||||
|
||||
it("renders ChatHeader guest and member branches", () => {
|
||||
const guestHtml = renderToStaticMarkup(<ChatHeader isGuest={true} />);
|
||||
const memberHtml = renderToStaticMarkup(
|
||||
const guestHtml = renderWithCharacter(<ChatHeader isGuest={true} />);
|
||||
const memberHtml = renderWithCharacter(
|
||||
<ChatHeader isGuest={false} offerBanner={<div>Offer</div>} />,
|
||||
);
|
||||
const memberWithHintHtml = renderToStaticMarkup(
|
||||
const memberWithHintHtml = renderWithCharacter(
|
||||
<ChatHeader isGuest={false} showBrowserHint />,
|
||||
);
|
||||
const guestWithHintHtml = renderToStaticMarkup(
|
||||
const guestWithHintHtml = renderWithCharacter(
|
||||
<ChatHeader isGuest={true} showBrowserHint />,
|
||||
);
|
||||
|
||||
@@ -369,3 +375,11 @@ describe("chat Tailwind components", () => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function renderWithCharacter(node: ReactNode, slug = "elio"): string {
|
||||
const character = getCharacterBySlug(slug);
|
||||
if (!character) throw new Error(`Missing ${slug} character fixture`);
|
||||
return renderToStaticMarkup(
|
||||
<CharacterProvider character={character}>{node}</CharacterProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
|
||||
import type { UiMessage } from "@/stores/chat/ui-message";
|
||||
import { DEFAULT_CHARACTER_ID } from "@/data/constants/character";
|
||||
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
|
||||
|
||||
import {
|
||||
@@ -46,7 +45,7 @@ const ReplyingAnimation = lazy(() =>
|
||||
);
|
||||
|
||||
export interface ChatAreaProps {
|
||||
characterId?: string;
|
||||
characterId: string;
|
||||
messages: readonly UiMessage[];
|
||||
isReplyingAI: boolean;
|
||||
scrollToBottomSignal?: number;
|
||||
@@ -63,7 +62,7 @@ export interface ChatAreaProps {
|
||||
}
|
||||
|
||||
export function ChatArea({
|
||||
characterId = DEFAULT_CHARACTER_ID,
|
||||
characterId,
|
||||
messages,
|
||||
isReplyingAI,
|
||||
scrollToBottomSignal = 0,
|
||||
|
||||
@@ -9,10 +9,9 @@
|
||||
*/
|
||||
|
||||
import { ChatMediaImage } from "./chat-media-image";
|
||||
import { DEFAULT_CHARACTER_ID } from "@/data/constants/character";
|
||||
|
||||
export interface ImageBubbleProps {
|
||||
characterId?: string;
|
||||
characterId: string;
|
||||
messageId?: string;
|
||||
imageUrl: string; // base64 data URI 或 URL
|
||||
imagePaywalled?: boolean;
|
||||
@@ -20,7 +19,7 @@ export interface ImageBubbleProps {
|
||||
}
|
||||
|
||||
export function ImageBubble({
|
||||
characterId = DEFAULT_CHARACTER_ID,
|
||||
characterId,
|
||||
messageId,
|
||||
imageUrl,
|
||||
imagePaywalled = false,
|
||||
|
||||
Reference in New Issue
Block a user