From 27960109717be853807179d08aa60531c9a61e0f Mon Sep 17 00:00:00 2001 From: chenhang Date: Fri, 17 Jul 2026 11:42:31 +0800 Subject: [PATCH] feat(characters): support character-scoped conversations --- barrelsby.json | 2 + docs/backend/FRONTEND_MULTI_CHARACTER_API.md | 544 ++++++++++++++++++ src/app/chat/chat-screen.tsx | 3 + src/app/chat/components/chat-area.tsx | 6 + src/app/chat/components/chat-media-image.tsx | 3 + .../components/fullscreen-image-viewer.tsx | 4 + src/app/chat/components/image-bubble.tsx | 4 + src/app/chat/components/message-bubble.tsx | 4 + src/app/chat/components/message-content.tsx | 4 + src/app/chat/components/voice-bubble.tsx | 3 + .../hooks/use-splash-latest-message-sync.ts | 9 +- src/data/constants/character.ts | 2 + .../dto/character/__tests__/character.test.ts | 42 ++ src/data/dto/character/character.ts | 29 + src/data/dto/character/characters_response.ts | 33 ++ src/data/dto/character/index.ts | 2 + .../__tests__/unlock_private_request.test.ts | 13 +- src/data/dto/chat/request/index.ts | 1 + .../dto/chat/request/send_message_request.ts | 1 + .../chat/request/unlock_history_request.ts | 22 + .../chat/request/unlock_private_request.ts | 1 + .../chat/requests/send-image-message.json | 1 + .../mock/chat/requests/send-text-message.json | 1 + .../chat/requests/unlock-private-message.json | 1 + .../__tests__/chat_cache_identity.test.ts | 14 +- .../chat_media_cache_coordinator.test.ts | 37 +- .../chat_media_cache_helpers.test.ts | 43 +- src/data/repositories/character_repository.ts | 18 + src/data/repositories/chat_cache_identity.ts | 21 + .../chat_media_cache_coordinator.ts | 28 +- .../repositories/chat_media_cache_helpers.ts | 10 +- .../repositories/chat_remote_data_source.ts | 14 +- src/data/repositories/chat_repository.ts | 19 +- src/data/repositories/index.ts | 2 + .../interfaces/icharacter_repository.ts | 6 + .../interfaces/ichat_repository.ts | 13 +- src/data/repositories/interfaces/index.ts | 1 + .../interfaces/iprivate_room_repository.ts | 2 +- src/data/schemas/character/character.ts | 13 + .../schemas/character/characters_response.ts | 10 + src/data/schemas/character/index.ts | 2 + src/data/schemas/chat/request/index.ts | 1 + .../chat/request/send_message_request.ts | 1 + .../chat/request/unlock_history_request.ts | 12 + .../chat/request/unlock_private_request.ts | 1 + .../api/__tests__/character_api.test.ts | 45 ++ .../api/__tests__/multi_character_api.test.ts | 121 ++++ src/data/services/api/api_contract.json | 3 +- src/data/services/api/api_path.ts | 3 + src/data/services/api/character_api.ts | 14 + src/data/services/api/chat_api.ts | 14 +- src/data/services/api/index.ts | 1 + src/data/services/api/private_room_api.ts | 5 +- .../__tests__/splash_latest_message.test.ts | 18 +- src/lib/chat/chat_media_cache_client.ts | 1 + src/lib/chat/splash_latest_message.ts | 27 +- src/lib/chat/use_cached_chat_media_url.ts | 10 +- src/providers/chat-route-providers.tsx | 13 +- src/providers/private-room-route-provider.tsx | 17 +- .../chat/__tests__/chat-helpers.test.ts | 2 + .../chat/__tests__/chat-history-flow.test.ts | 41 +- .../chat/__tests__/chat-machine.test-utils.ts | 45 +- .../chat/__tests__/chat-send-flow.test.ts | 59 +- .../chat/__tests__/chat-session-flow.test.ts | 31 +- .../chat/__tests__/chat-unlock-flow.test.ts | 21 +- src/stores/chat/chat-context.tsx | 16 +- src/stores/chat/chat-history-sync.ts | 23 +- src/stores/chat/chat-machine.ts | 7 +- src/stores/chat/chat-state.ts | 61 +- src/stores/chat/machine/actors/history.ts | 106 ++-- src/stores/chat/machine/actors/send.ts | 25 +- src/stores/chat/machine/actors/unlock.ts | 18 +- src/stores/chat/machine/send-flow.ts | 6 +- src/stores/chat/machine/session-flow.ts | 16 +- src/stores/chat/machine/setup.ts | 5 + src/stores/chat/machine/unlock-flow.ts | 11 +- .../__tests__/private-room-album-flow.test.ts | 21 + .../private-room-machine.test-utils.ts | 17 +- src/stores/private-room/helper/albums.ts | 1 - .../private-room/machine/actors/albums.ts | 11 +- src/stores/private-room/machine/album-flow.ts | 1 + src/stores/private-room/machine/setup.ts | 5 + .../private-room/private-room-context.tsx | 14 +- .../private-room/private-room-machine.ts | 9 +- src/stores/private-room/private-room-state.ts | 29 +- 85 files changed, 1645 insertions(+), 251 deletions(-) create mode 100644 docs/backend/FRONTEND_MULTI_CHARACTER_API.md create mode 100644 src/data/constants/character.ts create mode 100644 src/data/dto/character/__tests__/character.test.ts create mode 100644 src/data/dto/character/character.ts create mode 100644 src/data/dto/character/characters_response.ts create mode 100644 src/data/dto/character/index.ts create mode 100644 src/data/dto/chat/request/unlock_history_request.ts create mode 100644 src/data/repositories/character_repository.ts create mode 100644 src/data/repositories/interfaces/icharacter_repository.ts create mode 100644 src/data/schemas/character/character.ts create mode 100644 src/data/schemas/character/characters_response.ts create mode 100644 src/data/schemas/character/index.ts create mode 100644 src/data/schemas/chat/request/unlock_history_request.ts create mode 100644 src/data/services/api/__tests__/character_api.test.ts create mode 100644 src/data/services/api/__tests__/multi_character_api.test.ts create mode 100644 src/data/services/api/character_api.ts diff --git a/barrelsby.json b/barrelsby.json index 2ece9f97..e78fef4d 100644 --- a/barrelsby.json +++ b/barrelsby.json @@ -14,6 +14,7 @@ "./src/data/services/api", "./src/data/dto/auth", "./src/data/dto/chat", + "./src/data/dto/character", "./src/data/dto/feedback", "./src/data/dto/feedback/response", "./src/data/dto/metrics", @@ -22,6 +23,7 @@ "./src/data/schemas/auth/request", "./src/data/schemas/auth/response", "./src/data/schemas/chat", + "./src/data/schemas/character", "./src/data/schemas/chat/request", "./src/data/schemas/chat/response", "./src/data/schemas/feedback", diff --git a/docs/backend/FRONTEND_MULTI_CHARACTER_API.md b/docs/backend/FRONTEND_MULTI_CHARACTER_API.md new file mode 100644 index 00000000..f232c1aa --- /dev/null +++ b/docs/backend/FRONTEND_MULTI_CHARACTER_API.md @@ -0,0 +1,544 @@ +# 单角色应用迁移为多角色聊天架构 + +## 1. 文档目标 + +本文档定义 CozSweet 从单角色 Elio 迁移到多角色架构时,后端需要完成的接口、数据模型和兼容策略调整。 + +迁移目标: + +1. 后端提供统一角色目录。 +2. 同一用户可以分别与多个角色聊天。 +3. 不同角色的聊天历史、媒体、锁内容、私密相册和打赏归属必须隔离。 +4. 钱包、VIP、支付套餐和每日免费额度继续按用户全局共享。 +5. 现有客户端和历史数据继续默认归属于 Elio。 + +本文档中的标准角色标识: + +| 类型 | Elio 示例 | 用途 | +| --- | --- | --- | +| `id` | `character_elio` | 数据库关联、API 请求和缓存隔离 | +| `slug` | `elio` | 前端 URL 和外部入口 | + +后端业务接口必须使用 `id`,不能使用可能变化的展示名称。 + +环境地址: + +| 环境 | API Base URL | +| --- | --- | +| test 测试环境 | `https://testapi.banlv-ai.com` | +| pro 预发环境 | `https://proapi.banlv-ai.com` | +| production 生产环境 | `https://api.banlv-ai.com` | + +## 2. 角色目录接口 + +### 2.1 请求 + +```http +GET /api/characters +Authorization: Bearer +``` + +接口支持正式用户 Login Token 和游客 Guest Token。 + +### 2.2 成功响应 + +```json +{ + "success": true, + "message": "success", + "data": { + "items": [ + { + "id": "character_elio", + "slug": "elio", + "displayName": "Elio Silvestri", + "avatarUrl": "https://cdn.example.com/characters/elio/avatar.jpg" + }, + { + "id": "character_aria", + "slug": "aria", + "displayName": "Aria", + "avatarUrl": "https://cdn.example.com/characters/aria/avatar.jpg" + } + ] + } +} +``` + +公开字段只有: + +| 字段 | 类型 | 是否必填 | 说明 | +| --- | --- | --- | --- | +| `id` | string | 是 | 稳定、唯一、不可变的角色业务 ID。 | +| `slug` | string | 是 | 稳定、唯一、URL 安全的角色标识。 | +| `displayName` | string | 是 | 前端展示名称。 | +| `avatarUrl` | string | 是 | HTTPS 头像地址。 | + +后端只返回启用角色,并按照后台运营顺序排列 `items`。前端直接使用数组顺序,不执行二次排序。 + +响应中不要增加以下字段: + +- `enabled` +- `sortOrder` +- `coverUrl` +- `chatBackgroundUrl` +- `tagline` +- `emptyChatGreeting` +- `capabilities` +- 创建人、更新时间等后台管理字段 + +当前约定所有返回的角色都支持聊天、私密空间和打赏。 + +### 2.3 角色字段规则 + +- `id` 建议使用不可枚举 UUID 或稳定业务 ID,最大长度 64。 +- `slug` 只允许小写字母、数字和连字符,建议校验 `^[a-z0-9]+(?:-[a-z0-9]+)*$`。 +- `slug` 发布后不能修改;必须修改时应保留旧 slug 重定向映射。 +- `displayName` 去除首尾空白后不能为空,建议最大长度 80。 +- `avatarUrl` 必须为有效 HTTPS URL。 +- `id` 和 `slug` 都需要数据库唯一约束。 + +角色目录不需要分页,也不返回 `total`。 + +## 3. 聊天接口调整 + +### 3.1 通用规则 + +以下接口新增标准字段 `characterId`: + +```text +POST /api/chat/send +GET /api/chat/history +POST /api/chat/unlock-private +POST /api/chat/unlock-history +``` + +`characterId` 使用角色目录中的 `id`,例如 `character_elio`,不能传 slug 或显示名称。 + +后端必须使用以下维度定位聊天数据: + +```text +正式用户:userId + characterId +游客:guest identity/device identity + characterId +``` + +任何历史查询、消息发送、解锁和配额判断都不能只按用户查找。 + +### 3.2 发送消息 + +```http +POST /api/chat/send +Content-Type: application/json +Authorization: Bearer +``` + +现有请求体增加 `characterId`,其他字段保持原协议: + +```json +{ + "characterId": "character_elio", + "message": "Hello Elio", + "image": "", + "imageId": "", + "imageThumbUrl": "", + "imageMediumUrl": "", + "imageOriginalUrl": "", + "imageWidth": 0, + "imageHeight": 0, + "useWebSocket": false +} +``` + +后端处理要求: + +1. 校验角色存在且已启用。 +2. 使用 `characterId` 选择对应角色 Prompt、模型配置和聊天上下文。 +3. 只加载该用户与该角色的历史。 +4. 新消息和生成结果必须写入相同 `characterId`。 +5. 响应继续沿用现有 `ChatSendResponse`,不额外返回前端不使用的角色字段。 + +如果后端启用 WebSocket,连接握手或每条发送帧也必须携带 `characterId`,不能依赖服务端进程内的“当前角色”状态。 + +### 3.3 获取聊天历史 + +```http +GET /api/chat/history?characterId=character_elio&limit=50&offset=0 +Authorization: Bearer +``` + +| 参数 | 类型 | 是否必填 | 说明 | +| --- | --- | --- | --- | +| `characterId` | string | 是 | 角色业务 ID。 | +| `limit` | integer | 否 | 保持现有分页规则。 | +| `offset` | integer | 否 | 保持现有分页规则。 | + +响应结构和分页字段保持不变。`messages`、`total` 和 `offset` 必须只统计当前角色。 + +禁止在当前角色历史中返回其他角色的消息,即使消息属于同一用户。 + +### 3.4 解锁单条消息 + +```http +POST /api/chat/unlock-private +Content-Type: application/json +Authorization: Bearer +``` + +请求体增加 `characterId`: + +```json +{ + "characterId": "character_elio", + "messageId": "message-id", + "lockType": "voice_message", + "clientLockId": "client-generated-id" +} +``` + +现有 `messageId`、`lockType` 和 `clientLockId` 规则保持不变。 + +后端必须验证: + +- 已存在消息的 `character_id` 与请求 `characterId` 一致。 +- 临时锁消息不存在 `messageId` 时,新建消息必须写入请求角色。 +- `clientLockId` 的幂等范围至少包含身份和角色,推荐唯一键: + +```text +identity + character_id + client_lock_id +``` + +跨角色解锁返回 `CHARACTER_MISMATCH`,不能扣积分,也不能修改消息。 + +### 3.5 一键解锁历史 + +```http +POST /api/chat/unlock-history +Content-Type: application/json +Authorization: Bearer +``` + +请求体由空请求调整为: + +```json +{ + "characterId": "character_elio" +} +``` + +后端只能统计和解锁当前角色的锁消息。`requiredCredits`、解锁数量和返回 messages 都必须按当前角色计算。 + +## 4. 角色最新消息接口 + +角色列表需要一次性获取各角色的最新消息,避免对每个角色分别请求 history。 + +### 4.1 请求 + +```http +GET /api/chat/previews +Authorization: Bearer +``` + +### 4.2 响应 + +```json +{ + "success": true, + "message": "success", + "data": { + "items": [ + { + "characterId": "character_elio", + "message": { + "id": "message-id", + "role": "assistant", + "type": "text", + "content": "How was your day?", + "createdAt": "2026-07-17T10:00:00Z" + } + }, + { + "characterId": "character_aria", + "message": null + } + ] + } +} +``` + +- `items` 只包含当前启用角色,并保持角色目录顺序。 +- `message` 使用现有 ChatMessage wire 结构;没有历史时返回 `null`。 +- 锁消息继续遵循现有内容隐藏规则,不能通过预览接口泄露 URL 或锁定内容。 +- 查询应批量完成,避免后端内部出现按角色逐条查询的 N+1 问题。 + +## 5. 私密空间接口调整 + +### 5.1 获取相册 + +标准请求改为: + +```http +GET /api/private-room/albums?characterId=character_elio&limit=20 +Authorization: Bearer +``` + +兼容期继续接受旧参数: + +```http +GET /api/private-room/albums?character=elio&limit=20 +``` + +参数优先级: + +1. 存在 `characterId` 时使用标准 ID。 +2. 兼容期仅存在 `character` 时,将旧 slug 解析为角色 ID。 +3. 两者同时存在但指向不同角色时返回 `CHARACTER_MISMATCH`。 +4. 两者都缺失时,兼容期默认 Elio。 + +响应结构保持不变,不增加前端未使用的角色字段。 + +### 5.2 解锁相册 + +接口路径保持不变: + +```http +POST /api/private-room/albums/{albumId}/unlock +``` + +`albumId` 已能唯一确定角色,因此请求体不增加 `characterId`。后端必须根据 album 关联的角色进行权限、积分和解锁记录校验。 + +## 6. Tip 支付接口调整 + +### 6.1 创建订单 + +现有接口: + +```http +POST /api/payment/create-order +``` + +请求体增加可选字段 `recipientCharacterId`: + +```json +{ + "planId": "tip_coffee_medium", + "payChannel": "stripe", + "autoRenew": false, + "recipientCharacterId": "character_elio" +} +``` + +后端必须先根据 `planId` 判断订单类型: + +| 订单类型 | `recipientCharacterId` 规则 | +| --- | --- | +| Tip | 必填,并校验角色存在且启用 | +| VIP | 忽略,不保存 | +| Top-up | 忽略,不保存 | + +Tip 订单和最终支付记录需要保存 `recipient_character_id`,用于归属、统计和后续展示。 + +支付套餐、创建订单响应和订单状态响应不增加角色字段。 + +## 7. 数据模型调整 + +### 7.1 Characters 表 + +后台建议至少保存: + +| 字段 | 说明 | +| --- | --- | +| `id` | 主键,稳定角色 ID。 | +| `slug` | 唯一 URL 标识。 | +| `display_name` | 展示名称。 | +| `avatar_url` | 头像地址。 | +| `enabled` | 是否对用户启用,仅后台使用。 | +| `sort_order` | 运营排序,仅后台使用。 | +| `created_at` | 创建时间。 | +| `updated_at` | 更新时间。 | + +`enabled` 和 `sort_order` 是后端管理字段,不返回前端。 + +### 7.2 业务表 + +以下数据增加 `character_id` 外键: + +- conversations 或等价会话表; +- chat messages; +- 锁消息、解锁记录和 `clientLockId` 幂等记录; +- private albums 及相册解锁记录; +- Tip 订单或 Tip 业务记录。 + +推荐索引: + +```text +(user_id, character_id, created_at) +(guest_id, character_id, created_at) +(character_id, enabled) +(identity, character_id, client_lock_id) UNIQUE +``` + +如果当前系统没有 conversations 表,可以继续使用 messages,但所有历史查询必须显式包含 `character_id`。 + +## 8. 历史数据迁移 + +上线新接口前执行: + +1. 创建 Elio 角色,固定 `id=character_elio`、`slug=elio`。 +2. 为相关业务表增加可空 `character_id`。 +3. 将现有消息、锁记录、相册和 Tip 记录全部回填为 `character_elio`。 +4. 检查不存在空角色或无法关联的数据。 +5. 创建联合索引和外键。 +6. 将必要业务表的 `character_id` 调整为非空。 + +回填前后需要核对每张表的记录数量,迁移不能删除历史聊天或解锁状态。 + +## 9. 兼容发布策略 + +### 第一阶段:后端兼容 + +- 上线角色目录和新字段。 +- Chat 请求缺少 `characterId` 时暂时默认 `character_elio`。 +- Private Room 缺少角色时暂时默认 Elio。 +- Tip 旧客户端缺少 recipient 时,旧 Tip 订单暂时默认 Elio。 +- 记录缺少角色参数的调用次数和客户端版本。 + +### 第二阶段:前端迁移 + +- 新前端所有请求显式发送角色 ID。 +- 动态路由和支付回跳携带角色上下文。 +- 观察错误率、跨角色校验和旧请求占比。 + +### 第三阶段:收紧协议 + +- Chat 接口缺少 `characterId` 时返回 `CHARACTER_REQUIRED`。 +- Tip 订单缺少 recipient 时返回 `CHARACTER_REQUIRED`。 +- 删除 Private Room 旧 `character` 参数。 +- 删除默认 Elio 的接口兼容逻辑。 + +由于 PWA 和浏览器可能长期缓存旧前端,第三阶段只能在旧请求流量降至可接受水平后执行。 + +只有“完全缺少角色字段”的旧请求可以在兼容期默认 Elio。显式传入不存在、禁用或不匹配角色时不得回退 Elio。 + +## 10. 错误响应 + +统一失败 envelope: + +```json +{ + "success": false, + "message": "Character is not available", + "error": "CHARACTER_DISABLED" +} +``` + +新增错误: + +| HTTP 状态 | error | 场景 | +| ---: | --- | --- | +| `400` | `CHARACTER_REQUIRED` | 收紧协议后缺少角色字段。 | +| `404` | `CHARACTER_NOT_FOUND` | `id` 或兼容 slug 不存在。 | +| `403` | `CHARACTER_DISABLED` | 角色存在但未启用。 | +| `409` | `CHARACTER_MISMATCH` | 消息、相册或重复参数属于不同角色。 | + +角色错误不能扣积分、创建消息、创建订单或修改任何解锁状态。 + +## 11. 安全和一致性要求 + +- 角色 ID 必须由后端查询验证,不能信任客户端展示信息。 +- 用户只能读取自己的某个角色会话,不能通过替换 characterId 读取其他用户数据。 +- messageId、albumId 和 clientLockId 必须同时校验身份与角色归属。 +- 角色禁用后不能继续发送、解锁、创建相册订单或 Tip 订单。 +- 角色禁用不删除历史数据,恢复启用后历史仍可读取。 +- 日志可以记录 `characterId`,但不能记录聊天正文、Token 或私密媒体 URL。 +- Analytics 和业务统计统一使用角色 ID,不使用 displayName。 + +## 12. 联调验收 + +后端完成后至少验证: + +1. `GET /api/characters` 只返回 4 个公开字段。 +2. 角色目录只包含启用角色,并严格按照后台排序。 +3. Login Token 和 Guest Token 均可获取角色目录并聊天。 +4. 同一用户在 Elio 和其他角色中的历史完全隔离。 +5. 发送消息、历史分页、单条解锁和历史解锁都限定当前角色。 +6. 使用 Elio 的 messageId 配合其他角色 ID 解锁时返回 `CHARACTER_MISMATCH`,且不扣积分。 +7. Private Room 的标准 characterId 与旧 character slug 在兼容期结果一致。 +8. Tip 订单正确保存 `recipient_character_id`,VIP 和 Top-up 不保存。 +9. 角色预览接口一次返回各启用角色的最新消息,没有历史时返回 `null`。 +10. 禁用角色从目录消失,直接访问业务接口返回 `CHARACTER_DISABLED`。 +11. 缺少角色字段的旧请求在兼容期仍进入 Elio。 +12. 历史数据回填前后记录数量、积分和解锁状态一致。 + +## 13. OpenAPI 参考 + +```yaml +paths: + /api/characters: + get: + summary: List enabled characters in display order + security: + - bearerAuth: [] + responses: + "200": + description: Enabled characters + content: + application/json: + schema: + type: object + required: [success, data] + properties: + success: + type: boolean + const: true + data: + type: object + required: [items] + properties: + items: + type: array + items: + type: object + required: [id, slug, displayName, avatarUrl] + additionalProperties: false + properties: + id: + type: string + slug: + type: string + pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" + displayName: + type: string + avatarUrl: + type: string + format: uri + /api/chat/history: + get: + parameters: + - in: query + name: characterId + required: true + schema: + type: string + - in: query + name: limit + required: false + schema: + type: integer + - in: query + name: offset + required: false + schema: + type: integer + /api/chat/unlock-history: + post: + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [characterId] + properties: + characterId: + type: string +``` diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx index aed2ca33..75c05226 100644 --- a/src/app/chat/chat-screen.tsx +++ b/src/app/chat/chat-screen.tsx @@ -52,6 +52,7 @@ export function ChatScreen() { const authState = useAuthState(); const authDispatch = useAuthDispatch(); useSplashLatestMessageSync({ + characterId: state.characterId, historyLoaded: state.historyLoaded, loginStatus: authState.loginStatus, messages: state.historyMessages, @@ -182,6 +183,7 @@ export function ChatScreen() { /> ); export interface ChatAreaProps { + characterId?: string; messages: readonly UiMessage[]; isReplyingAI: boolean; scrollToBottomSignal?: number; @@ -61,6 +63,7 @@ export interface ChatAreaProps { } export function ChatArea({ + characterId = DEFAULT_CHARACTER_ID, messages, isReplyingAI, scrollToBottomSignal = 0, @@ -235,6 +238,7 @@ export function ChatArea({ {renderMessagesWithDateHeaders( + characterId, messages, getMessageKey, isUnlockingMessage, @@ -270,6 +274,7 @@ function isNearBottom(scrollNode: HTMLElement): boolean { /** 渲染消息列表(按日期分组插入分隔条) */ function renderMessagesWithDateHeaders( + characterId: string, messages: readonly UiMessage[], getMessageKey: ChatMessageKeyResolver, isUnlockingMessage?: boolean, @@ -284,6 +289,7 @@ function renderMessagesWithDateHeaders( ) : ( (null); const { mediaUrl, isUsingCachedMedia, reportMediaError } = useCachedChatMediaUrl({ + characterId, messageId, remoteUrl, kind: "image", diff --git a/src/app/chat/components/fullscreen-image-viewer.tsx b/src/app/chat/components/fullscreen-image-viewer.tsx index 3fb92f5e..317bf49b 100644 --- a/src/app/chat/components/fullscreen-image-viewer.tsx +++ b/src/app/chat/components/fullscreen-image-viewer.tsx @@ -18,6 +18,7 @@ import { ChatMediaImage } from "./chat-media-image"; import styles from "./fullscreen-image-viewer.module.css"; export interface FullscreenImageViewerProps { + characterId: string; messageId?: string; imageUrl: string; imagePaywalled?: boolean; @@ -27,6 +28,7 @@ export interface FullscreenImageViewerProps { } export function FullscreenImageViewer({ + characterId, messageId, imageUrl, imagePaywalled = false, @@ -51,6 +53,7 @@ export function FullscreenImageViewer({ aria-label="Locked fullscreen image" >