From 9deb320cf65f561e69631c91abf8d42bf28b9ca3 Mon Sep 17 00:00:00 2001 From: chenhang Date: Tue, 21 Jul 2026 18:08:12 +0800 Subject: [PATCH] feat(profile)!: replace sidebar and add avatar navigation Rename the global Sidebar route, UI, assets, analytics, and payment return context to Profile. Add accessible message-avatar navigation and preserve the source character across auth and logout flows. BREAKING CHANGE: /sidebar has been removed; use /profile instead. --- docs/backend/FRONTEND_PAYMENT_API.md | 2 +- e2e/fixtures/helpers/auth.ts | 2 +- e2e/fixtures/helpers/chat.ts | 2 +- .../email-login-from-other-options.spec.ts | 2 +- ...ar.spec.ts => logout-from-profile.spec.ts} | 10 +- .../mock/chat/chat-avatar-navigation.spec.ts | 86 ++++++++++++++++++ .../email-login-from-other-options.spec.ts | 2 +- package.json | 2 +- .../{sidebar => profile}/ic_user_vip.png | Bin .../ic_user_voicemessage.png | Bin .../{sidebar => profile}/pic_bg_vip.png | Bin .../__tests__/tailwind-components.test.tsx | 30 ++++++ src/app/_components/avatar-interaction.ts | 11 +++ src/app/_components/character-avatar.tsx | 65 +++++++++---- src/app/_components/index.ts | 1 + src/app/_components/user-message-avatar.tsx | 54 +++++++---- src/app/chat/chat-screen.tsx | 23 ++++- .../__tests__/tailwind-components.test.tsx | 33 +++++-- src/app/chat/components/chat-area.tsx | 10 ++ src/app/chat/components/chat-header.tsx | 14 +-- src/app/chat/components/message-avatar.tsx | 51 ++++++++--- src/app/chat/components/message-bubble.tsx | 15 ++- .../__tests__/coins-rules-screen.test.tsx | 4 +- src/app/coins-rules/coins-rules-screen.tsx | 4 +- .../__tests__/feedback-screen.test.tsx | 6 +- src/app/feedback/feedback-screen.tsx | 12 +-- .../__tests__/profile-screen.test.tsx} | 14 +-- .../__tests__/profile-view-model.test.ts} | 32 +++---- .../__tests__/use-pwa-install-entry.test.ts | 0 .../__tests__/tailwind-components.test.tsx | 2 +- .../{sidebar => profile}/components/index.ts | 2 +- .../components/profile-screen.module.css} | 0 .../profile-wallet-card.module.css} | 0 .../components/profile-wallet-card.tsx} | 14 +-- .../components/user-header.tsx | 18 ++-- src/app/{sidebar => profile}/page.tsx | 6 +- .../profile-screen.tsx} | 38 ++++---- .../profile-view-model.ts} | 52 ++++++----- .../use-profile-user-bootstrap.ts} | 6 +- .../use-pwa-install-entry.ts | 0 .../__tests__/tailwind-components.test.tsx | 2 +- .../splash/components/splash-background.tsx | 2 +- src/app/splash/splash-screen.tsx | 2 +- .../__tests__/subscription-page.test.tsx | 4 +- .../payment/pending_payment_order_storage.ts | 2 +- .../payment_analytics_context.test.ts | 4 +- .../analytics/payment_analytics_context.ts | 8 +- .../__tests__/external_entry.test.ts | 2 +- .../__tests__/subscription_exit.test.ts | 16 ++-- src/lib/navigation/subscription_exit.ts | 10 +- .../__tests__/payment_search_params.test.ts | 2 +- .../__tests__/pending_payment_order.test.ts | 14 +-- src/lib/payment/payment_search_params.ts | 2 +- .../__tests__/global-route-context.test.ts | 8 +- .../legacy-global-route-redirects.test.ts | 5 - .../__tests__/logout-navigation.test.ts | 26 ++++++ .../__tests__/navigation-resolver.test.ts | 24 +++-- src/router/__tests__/route-meta.test.ts | 4 +- src/router/app-navigation-guard.tsx | 7 ++ src/router/global-route-context.ts | 6 +- src/router/legacy-global-route-redirects.ts | 5 - src/router/logout-navigation.ts | 11 +++ src/router/navigation-types.ts | 2 +- src/router/route-meta.ts | 2 +- src/router/routes.ts | 4 +- src/tokens/colors.css | 6 +- src/tokens/responsive.css | 16 ++-- 67 files changed, 565 insertions(+), 256 deletions(-) rename e2e/specs/mock/auth/{logout-from-sidebar.spec.ts => logout-from-profile.spec.ts} (80%) create mode 100644 e2e/specs/mock/chat/chat-avatar-navigation.spec.ts rename public/images/{sidebar => profile}/ic_user_vip.png (100%) rename public/images/{sidebar => profile}/ic_user_voicemessage.png (100%) rename public/images/{sidebar => profile}/pic_bg_vip.png (100%) create mode 100644 src/app/_components/avatar-interaction.ts rename src/app/{sidebar/__tests__/sidebar-screen.test.tsx => profile/__tests__/profile-screen.test.tsx} (90%) rename src/app/{sidebar/__tests__/sidebar-view-model.test.ts => profile/__tests__/profile-view-model.test.ts} (77%) rename src/app/{sidebar => profile}/__tests__/use-pwa-install-entry.test.ts (100%) rename src/app/{sidebar => profile}/components/__tests__/tailwind-components.test.tsx (96%) rename src/app/{sidebar => profile}/components/index.ts (69%) rename src/app/{sidebar/components/sidebar-screen.module.css => profile/components/profile-screen.module.css} (100%) rename src/app/{sidebar/components/sidebar-wallet-card.module.css => profile/components/profile-wallet-card.module.css} (100%) rename src/app/{sidebar/components/sidebar-wallet-card.tsx => profile/components/profile-wallet-card.tsx} (89%) rename src/app/{sidebar => profile}/components/user-header.tsx (86%) rename src/app/{sidebar => profile}/page.tsx (53%) rename src/app/{sidebar/sidebar-screen.tsx => profile/profile-screen.tsx} (84%) rename src/app/{sidebar/sidebar-view-model.ts => profile/profile-view-model.ts} (53%) rename src/app/{sidebar/use-sidebar-user-bootstrap.ts => profile/use-profile-user-bootstrap.ts} (82%) rename src/app/{sidebar => profile}/use-pwa-install-entry.ts (100%) create mode 100644 src/router/__tests__/logout-navigation.test.ts create mode 100644 src/router/logout-navigation.ts diff --git a/docs/backend/FRONTEND_PAYMENT_API.md b/docs/backend/FRONTEND_PAYMENT_API.md index 7dd2ccd9..aef9f094 100644 --- a/docs/backend/FRONTEND_PAYMENT_API.md +++ b/docs/backend/FRONTEND_PAYMENT_API.md @@ -224,7 +224,7 @@ payChannel = ezpay subscriptionType = vip | topup | tip giftCategory(仅 Tip,可空) giftPlanId(仅 Tip,可空) -returnTo = chat | private-room | sidebar(可选) +returnTo = chat | private-room | profile(可选) characterSlug(可选) createdAt ``` diff --git a/e2e/fixtures/helpers/auth.ts b/e2e/fixtures/helpers/auth.ts index a37c55e8..04e2dd58 100644 --- a/e2e/fixtures/helpers/auth.ts +++ b/e2e/fixtures/helpers/auth.ts @@ -11,7 +11,7 @@ export const splashStartChatButtonName = "Start Chatting"; export const defaultCharacterSlug = "elio"; export const defaultCharacterSplashPath = `/characters/${defaultCharacterSlug}/splash`; export const defaultCharacterChatPath = `/characters/${defaultCharacterSlug}/chat`; -export const defaultCharacterSidebarPath = `/sidebar?returnTo=%2Fcharacters%2F${defaultCharacterSlug}%2Fchat`; +export const defaultCharacterProfilePath = `/profile?returnTo=%2Fcharacters%2F${defaultCharacterSlug}%2Fchat`; export const defaultCharacterChatUrl = new RegExp( `/characters/${defaultCharacterSlug}/chat(?:\\?.*)?$`, ); diff --git a/e2e/fixtures/helpers/chat.ts b/e2e/fixtures/helpers/chat.ts index 330cb827..9bada90f 100644 --- a/e2e/fixtures/helpers/chat.ts +++ b/e2e/fixtures/helpers/chat.ts @@ -27,7 +27,7 @@ export async function enterChatFromSplash(page: Page, options: { expectedUrl?: R await expect(startChatButton).toBeVisible({ timeout }); await expect(startChatButton).toBeEnabled(); for (let attempt = 0; attempt < 3; attempt += 1) { - await startChatButton.click(); + await startChatButton.click({ timeout: 3_000 }).catch(() => {}); try { await expect(page).toHaveURL(expectedUrl, { timeout: 3_000 }); return; } catch { await page.waitForTimeout(500); } } await expect(page).toHaveURL(expectedUrl, { timeout }); diff --git a/e2e/specs/mock/auth/email-login-from-other-options.spec.ts b/e2e/specs/mock/auth/email-login-from-other-options.spec.ts index 99511ee6..e55239ab 100644 --- a/e2e/specs/mock/auth/email-login-from-other-options.spec.ts +++ b/e2e/specs/mock/auth/email-login-from-other-options.spec.ts @@ -31,5 +31,5 @@ test("user can email login from other sign-in options and return to chat", async expect(["desktop", "android"]).toContain(loginRequest?.postDataJSON().platform); await expect(page.getByRole("textbox", { name: "Message" })).toBeVisible(); - await expect(page.getByRole("button", { name: "Menu" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Profile" })).toBeVisible(); }); diff --git a/e2e/specs/mock/auth/logout-from-sidebar.spec.ts b/e2e/specs/mock/auth/logout-from-profile.spec.ts similarity index 80% rename from e2e/specs/mock/auth/logout-from-sidebar.spec.ts rename to e2e/specs/mock/auth/logout-from-profile.spec.ts index 27bb0484..215fb868 100644 --- a/e2e/specs/mock/auth/logout-from-sidebar.spec.ts +++ b/e2e/specs/mock/auth/logout-from-profile.spec.ts @@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test"; import { mockCoreApis } from "@e2e/fixtures/api-mocks"; import { clearBrowserState, - defaultCharacterSidebarPath, + defaultCharacterProfilePath, defaultCharacterSplashPath, defaultCharacterChatUrl, enterChatFromSplash, @@ -17,7 +17,7 @@ test.beforeEach(async ({ baseURL, context, page }) => { await mockCoreApis(page); }); -test("user can log out from the sidebar after email login", async ({ +test("user can log out from the profile after email login", async ({ page, }) => { await enterChatFromSplash(page); @@ -27,11 +27,11 @@ test("user can log out from the sidebar after email login", async ({ await expect(page).toHaveURL(/\/auth(?:\?.*)?$/); await switchToEmailSignIn(page); await submitEmailLogin(page, { expectedUrl: defaultCharacterChatUrl }); - await expect(page.getByRole("button", { name: "Menu" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Profile" })).toBeVisible(); - await page.getByRole("button", { name: "Menu" }).click(); + await page.getByRole("button", { name: "Profile" }).click(); await expect(page).toHaveURL( - new RegExp(defaultCharacterSidebarPath.replace("?", "\\?") + "$"), + new RegExp(defaultCharacterProfilePath.replace("?", "\\?") + "$"), ); const logoutRequestPromise = page.waitForRequest("**/api/auth/logout"); diff --git a/e2e/specs/mock/chat/chat-avatar-navigation.spec.ts b/e2e/specs/mock/chat/chat-avatar-navigation.spec.ts new file mode 100644 index 00000000..b29e5adb --- /dev/null +++ b/e2e/specs/mock/chat/chat-avatar-navigation.spec.ts @@ -0,0 +1,86 @@ +import { expect, test, type Page } from "@playwright/test"; + +import { mockCoreApis } from "@e2e/fixtures/api-mocks"; +import { + clearBrowserState, + defaultCharacterProfilePath, + dismissChatInterruptions, + enterChatFromSplash, + submitEmailLogin, + switchToEmailSignIn, +} from "@e2e/fixtures/test-helpers"; + +const characters = [ + { slug: "elio", displayName: "Elio Silvestri" }, + { slug: "maya", displayName: "Maya Tan" }, + { slug: "nayeli", displayName: "Nayeli Cervantes" }, +] as const; + +test.beforeEach(async ({ baseURL, context, page }) => { + await clearBrowserState(context, page, baseURL); + await mockCoreApis(page); +}); + +test("guest user avatar returns to Profile after sign-in", async ({ + page, +}) => { + await enterChatFromSplash(page, { timeout: 30_000 }); + await sendMessage(page, "Open my profile"); + + await page.getByRole("button", { name: "Open profile" }).click(); + + await expect(page).toHaveURL(/\/auth\?redirect=/); + expect(new URL(page.url()).searchParams.get("redirect")).toBe( + defaultCharacterProfilePath, + ); + await switchToEmailSignIn(page); + await submitEmailLogin(page, { + expectedUrl: new RegExp( + `${defaultCharacterProfilePath.replace("?", "\\?")}$`, + ), + }); +}); + +for (const character of characters) { + test(`${character.displayName} avatar opens the matching Private Room`, async ({ + page, + }) => { + await enterCharacterChat(page, character.slug); + await sendMessage(page, `Hello ${character.displayName}`); + + await page + .getByRole("button", { + name: `Open ${character.displayName}'s private room`, + }) + .last() + .click(); + + await expect(page).toHaveURL( + new RegExp(`/characters/${character.slug}/private-room(?:\\?.*)?$`), + ); + }); +} + +async function enterCharacterChat(page: Page, characterSlug: string) { + const chatPath = `/characters/${characterSlug}/chat`; + + await page.goto(`/characters/${characterSlug}/splash`); + await page.getByRole("button", { name: "Start Chatting" }).click(); + await expect(page).toHaveURL(new RegExp(`${chatPath}(?:\\?.*)?$`)); + await dismissChatInterruptions(page); + await expect(page.getByRole("textbox", { name: "Message" })).toBeEnabled({ + timeout: 20_000, + }); +} + +async function sendMessage(page: Page, message: string) { + await dismissChatInterruptions(page); + const messageInput = page.getByRole("textbox", { name: "Message" }); + + await expect(messageInput).toBeEnabled({ timeout: 20_000 }); + await messageInput.fill(message); + await messageInput.press("Enter"); + await expect(page.getByRole("button", { name: "Open profile" })).toBeVisible({ + timeout: 10_000, + }); +} diff --git a/e2e/specs/real/email-login-from-other-options.spec.ts b/e2e/specs/real/email-login-from-other-options.spec.ts index 101bf1c3..27737849 100644 --- a/e2e/specs/real/email-login-from-other-options.spec.ts +++ b/e2e/specs/real/email-login-from-other-options.spec.ts @@ -42,7 +42,7 @@ test.describe("pre-release email login smoke", () => { urlTimeout: 20_000, }); - await expect(page.getByRole("button", { name: "Menu" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Profile" })).toBeVisible(); await expect(page.getByRole("textbox", { name: "Message" })).toBeVisible(); }); }); diff --git a/package.json b/package.json index 9bbc58c3..ff780e24 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "test:e2e:chrome": "PLAYWRIGHT_USE_SYSTEM_CHROME=1 playwright test --project=mock-chromium", "test:e2e:headed": "playwright test --project=mock-chromium --headed", "test:e2e:ui": "playwright test --ui", - "test:e2e:mobile-smoke": "playwright test --project=mock-mobile-chrome e2e/specs/mock/auth/email-login-from-other-options.spec.ts e2e/specs/mock/auth/logout-from-sidebar.spec.ts e2e/specs/mock/chat/chat-send-token-refresh-retry.spec.ts e2e/specs/mock/unlock-message/image-unlock-insufficient-credits.spec.ts", + "test:e2e:mobile-smoke": "playwright test --project=mock-mobile-chrome e2e/specs/mock/auth/email-login-from-other-options.spec.ts e2e/specs/mock/auth/logout-from-profile.spec.ts e2e/specs/mock/chat/chat-send-token-refresh-retry.spec.ts e2e/specs/mock/unlock-message/image-unlock-insufficient-credits.spec.ts", "test:e2e:real": "E2E_REAL_BACKEND=1 PLAYWRIGHT_BASE_URL=${PLAYWRIGHT_BASE_URL:-https://frontend-test.banlv-ai.com} E2E_API_BASE_URL=${E2E_API_BASE_URL:-https://proapi.banlv-ai.com} playwright test --project=real-backend", "test:e2e:prod": "E2E_REAL_BACKEND=1 PLAYWRIGHT_BASE_URL=${PLAYWRIGHT_BASE_URL:-https://cozsweet.com} E2E_API_BASE_URL=${E2E_API_BASE_URL:-https://proapi.banlv-ai.com} playwright test --project=prod-smoke", "contract:check": "node scripts/contracts/check-openapi.mjs", diff --git a/public/images/sidebar/ic_user_vip.png b/public/images/profile/ic_user_vip.png similarity index 100% rename from public/images/sidebar/ic_user_vip.png rename to public/images/profile/ic_user_vip.png diff --git a/public/images/sidebar/ic_user_voicemessage.png b/public/images/profile/ic_user_voicemessage.png similarity index 100% rename from public/images/sidebar/ic_user_voicemessage.png rename to public/images/profile/ic_user_voicemessage.png diff --git a/public/images/sidebar/pic_bg_vip.png b/public/images/profile/pic_bg_vip.png similarity index 100% rename from public/images/sidebar/pic_bg_vip.png rename to public/images/profile/pic_bg_vip.png diff --git a/src/app/_components/__tests__/tailwind-components.test.tsx b/src/app/_components/__tests__/tailwind-components.test.tsx index 1a2b14a6..ee7524d3 100644 --- a/src/app/_components/__tests__/tailwind-components.test.tsx +++ b/src/app/_components/__tests__/tailwind-components.test.tsx @@ -79,4 +79,34 @@ describe("shared Tailwind components", () => { expect(guestHtml).toContain('aria-label="Guest avatar"'); expect(guestHtml).toContain("%2Fimages%2Favatar%2Fplaceholder.png"); }); + + it("renders interactive avatars as accessible buttons", () => { + const characterHtml = renderToStaticMarkup( + undefined} + />, + ); + const userHtml = renderToStaticMarkup( + undefined} + />, + ); + + expect(characterHtml).toContain(' + ); + } + return ( - - {alt} + + {image} ); } diff --git a/src/app/_components/index.ts b/src/app/_components/index.ts index edc9714a..b93bbeb9 100644 --- a/src/app/_components/index.ts +++ b/src/app/_components/index.ts @@ -3,5 +3,6 @@ */ export * from "./back-button"; +export * from "./avatar-interaction"; export * from "./character-avatar"; export * from "./user-message-avatar"; diff --git a/src/app/_components/user-message-avatar.tsx b/src/app/_components/user-message-avatar.tsx index e50a1364..0aa704df 100644 --- a/src/app/_components/user-message-avatar.tsx +++ b/src/app/_components/user-message-avatar.tsx @@ -2,19 +2,30 @@ import Image from "next/image"; -export interface UserMessageAvatarProps { +import type { AvatarInteractionProps } from "./avatar-interaction"; + +interface UserMessageAvatarVisualProps { avatarUrl?: string | null; className?: string; size?: number | string; } +export type UserMessageAvatarProps = UserMessageAvatarVisualProps & + AvatarInteractionProps; + export function UserMessageAvatar({ avatarUrl, className, size = 43, + onClick, + actionLabel, + analyticsKey, }: UserMessageAvatarProps) { const avatarClassName = [ "flex shrink-0 items-center justify-center overflow-hidden rounded-full border-2 border-(--color-avatar-border,#fbf3f5) bg-(--color-avatar-border,#fbf3f5) shadow-(--shadow-input-box,0_1px_2px_rgba(0,0,0,0.1))", + onClick + ? "cursor-pointer p-0 transition-transform duration-150 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-accent active:scale-96" + : undefined, className, ] .filter(Boolean) @@ -22,21 +33,30 @@ export function UserMessageAvatar({ const avatarStyle = { width: size, height: size }; const imageSize = typeof size === "number" ? size : 64; - if (avatarUrl && avatarUrl.length > 0) { + const hasUserAvatar = Boolean(avatarUrl && avatarUrl.length > 0); + const image = ( + {hasUserAvatar + ); + + if (onClick) { return ( -
- -
+ {image} + ); } @@ -44,15 +64,9 @@ export function UserMessageAvatar({
- Guest + {image}
); } diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx index d701b0ec..67377274 100644 --- a/src/app/chat/chat-screen.tsx +++ b/src/app/chat/chat-screen.tsx @@ -15,7 +15,9 @@ import { } 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 { buildGlobalPageUrl } from "@/router/global-route-context"; +import { resolveAuthenticatedNavigation } from "@/router/navigation-resolver"; +import { getCharacterRoutes, ROUTES } from "@/router/routes"; import { MobileShell } from "@/app/_components/core"; @@ -57,6 +59,10 @@ export function ChatScreen() { const refreshCharacterCatalog = characterCatalog.refresh; const defaultCharacterSlug = characterCatalog.defaultCharacter.slug; const characterRoutes = useActiveCharacterRoutes(); + const profileUrl = buildGlobalPageUrl( + ROUTES.profile, + characterRoutes.chat, + ); const searchParams = useSearchParams(); const state = useChatState(); const chatDispatch = useChatDispatch(); @@ -234,6 +240,19 @@ export function ChatScreen() { }); } + function handleOpenUserProfile(): void { + router.push( + resolveAuthenticatedNavigation({ + loginStatus: authState.loginStatus, + targetUrl: profileUrl, + }), + ); + } + + function handleOpenCharacterPrivateRoom(): void { + router.push(characterRoutes.privateRoom); + } + return (
{ chatDispatch({ type: "ChatLoadMoreHistoryRequested" }); }} diff --git a/src/app/chat/components/__tests__/tailwind-components.test.tsx b/src/app/chat/components/__tests__/tailwind-components.test.tsx index 2626bd17..39eb48ad 100644 --- a/src/app/chat/components/__tests__/tailwind-components.test.tsx +++ b/src/app/chat/components/__tests__/tailwind-components.test.tsx @@ -31,7 +31,6 @@ describe("chat Tailwind components", () => { , ); - expect(aiHtml).toContain('aria-label="AI avatar"'); expect(aiHtml).toContain("size-(--chat-avatar-size,43px)"); expect(aiHtml).toContain("size-full object-cover"); expect(aiHtml).toContain("%2Fimages%2Favatar%2Felio.png"); @@ -39,6 +38,28 @@ describe("chat Tailwind components", () => { expect(userHtml).toContain("%2Fuser-avatar.png"); }); + it("renders message avatar actions with explicit destinations", () => { + const aiHtml = renderWithCharacter( + undefined} />, + ); + const userHtml = renderWithCharacter( + undefined} />, + ); + + expect(aiHtml).toContain(' ) : null} diff --git a/src/app/chat/components/message-avatar.tsx b/src/app/chat/components/message-avatar.tsx index 576b1a7e..e2c567ef 100644 --- a/src/app/chat/components/message-avatar.tsx +++ b/src/app/chat/components/message-avatar.tsx @@ -1,32 +1,61 @@ "use client"; -import Image from "next/image"; -import { UserMessageAvatar } from "@/app/_components"; +import { CharacterAvatar, UserMessageAvatar } from "@/app/_components"; import { useActiveCharacter } from "@/providers/character-provider"; export interface MessageAvatarProps { isFromAI: boolean; userAvatarUrl?: string | null; + onClick?: () => void; } const AVATAR_CLASS_NAME = - "flex size-(--chat-avatar-size,43px) shrink-0 items-center justify-center overflow-hidden rounded-full border-2 border-(--color-avatar-border,#fbf3f5) bg-(--color-avatar-border,#fbf3f5) shadow-(--shadow-input-box,0_1px_2px_rgba(0,0,0,0.1))"; + "size-(--chat-avatar-size,43px) ring-2 ring-(--color-avatar-border,#fbf3f5) shadow-(--shadow-input-box,0_1px_2px_rgba(0,0,0,0.1))"; -export function MessageAvatar({ isFromAI, userAvatarUrl }: MessageAvatarProps) { +export function MessageAvatar({ + isFromAI, + userAvatarUrl, + onClick, +}: MessageAvatarProps) { const character = useActiveCharacter(); if (isFromAI) { - return ( -
- {character.displayName} -
+ ); + } + + return ( + + ); + } + + if (onClick) { + return ( + ); } diff --git a/src/app/chat/components/message-bubble.tsx b/src/app/chat/components/message-bubble.tsx index 32ae6665..18f20fee 100644 --- a/src/app/chat/components/message-bubble.tsx +++ b/src/app/chat/components/message-bubble.tsx @@ -32,6 +32,8 @@ export interface MessageBubbleProps { onUnlockVoiceMessage?: ChatMessageAction; onUnlockImageMessage?: ChatMessageAction; onOpenImage?: (displayMessageId: string) => void; + onUserAvatarClick?: () => void; + onCharacterAvatarClick?: () => void; } type ChatMessageAction = ( @@ -57,6 +59,8 @@ export function MessageBubble({ onUnlockVoiceMessage, onUnlockImageMessage, onOpenImage, + onUserAvatarClick, + onCharacterAvatarClick, }: MessageBubbleProps) { const avatarUrl = useUserSelector((state) => state.context.avatarUrl); @@ -67,7 +71,10 @@ export function MessageBubble({ data-chat-message-id={displayMessageId} aria-label="AI message" > - +