@appthen/sdk-react 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,11 +12,29 @@ function renderMutedMessageBody(status) {
12
12
  const label = status === 'deleted' ? '消息已删除' : '消息已撤回';
13
13
  return (_jsx("div", { className: "rounded-2xl border border-dashed border-border/60 bg-muted/20 px-3 py-2 text-sm text-muted-foreground", children: label }));
14
14
  }
15
+ /**
16
+ * 卡片消息内容判定(与宿主 messageCardRendering.isCardTemplateMessageContent 同构)。
17
+ * content.cardTemplate.templateId 是卡片段的唯一权威判定;kind 只是分派提示,
18
+ * 第三方写入方漏传/误标('text'、'cardTemplate' 等)时不应导致卡片降级为 JSON 文本。
19
+ */
20
+ function isCardTemplateMessageContent(message) {
21
+ const cardTemplate = message.content
22
+ ?.cardTemplate;
23
+ if (!cardTemplate || typeof cardTemplate !== 'object' || Array.isArray(cardTemplate)) {
24
+ return false;
25
+ }
26
+ const templateId = cardTemplate.templateId;
27
+ return typeof templateId === 'string' && templateId.length > 0;
28
+ }
15
29
  export function ChatHubMessageBody(props) {
16
30
  const { message, renderers } = props;
17
31
  if (message.status === 'deleted' || message.status === 'revoked') {
18
32
  return _jsx(_Fragment, { children: renderMutedMessageBody(message.status) });
19
33
  }
20
- const renderer = (message.kind && renderers?.[message.kind]) || renderers?.default;
34
+ const renderer = (message.kind && renderers?.[message.kind]) ||
35
+ // kind 误标兜底:content 权威判定为卡片时回落到卡片渲染器,
36
+ // 避免 kind !== 'card' 的卡片消息走 defaultRenderBody 渲染成原始 JSON。
37
+ (isCardTemplateMessageContent(message) ? renderers?.card : undefined) ||
38
+ renderers?.default;
21
39
  return _jsx(_Fragment, { children: renderer ? renderer(message) : defaultRenderBody(message) });
22
40
  }
package/dist/index.d.ts CHANGED
@@ -6,6 +6,17 @@ export { ChatHubMessageBody, type ChatHubMessageBodyRenderer, type ChatHubMessag
6
6
  export { createChatHubDefaultBodyRenderers, type ChatHubDefaultBodyRendererOptions, } from './ChatHubDefaultBodyRenderers';
7
7
  export { extractChatHubMessagePlainText, useChatHubMessageMutations, type ChatHubForwardDraftItem, type ForwardChatHubMessagesOptions, type ChatHubMutationActionState, type UseChatHubMessageMutationsOptions, type UseChatHubMessageMutationsResult, } from './useChatHubMessageMutations';
8
8
  export { useChatHubMessageActions, type ChatHubActionSurface, type ChatHubMessageActionItem, type UseChatHubMessageActionsOptions, type UseChatHubMessageActionsResult, } from './useChatHubMessageActions';
9
+ /**
10
+ * 会话列表刷新选项(P1 游标分页)。
11
+ * - `limit`:页大小(不传 = 全量,保持历史行为);
12
+ * - `search`:服务端搜索(标题 / conversationId 纯文本匹配)。
13
+ */
14
+ export interface RefreshConversationsOptions {
15
+ limit?: number;
16
+ search?: string;
17
+ }
18
+ /** 会话列表「加载更多」的默认页大小(与服务端 limit 上限 200 无冲突)。 */
19
+ export declare const DEFAULT_CONVERSATION_PAGE_SIZE = 30;
9
20
  export interface UseChatHubAppOptions {
10
21
  runtime: ChatHubAppRuntime;
11
22
  config: ChatHubAppConfig;
@@ -26,7 +37,12 @@ export interface UseChatHubAppResult {
26
37
  selectedSenderId: string;
27
38
  setSelectedSenderId: (subjectId: string) => void;
28
39
  bootstrapConversation: () => Promise<ChatHubBootstrapResult | null>;
29
- refreshConversations: () => Promise<void>;
40
+ refreshConversations: (options?: RefreshConversationsOptions) => Promise<void>;
41
+ /**
42
+ * 加载下一页会话(P1 游标分页):从当前列表最后一条构造 keyset 游标,
43
+ * 追加去重后返回本页新增条数(0 = 没有更多)。
44
+ */
45
+ loadMoreConversations: (limit?: number) => Promise<number>;
30
46
  refreshMessages: (conversationIdOrOptions?: string | (Partial<ListChatHubMessagesOptions> & {
31
47
  mode?: 'replace' | 'prepend' | 'append';
32
48
  })) => Promise<ConversationMessageRecord[]>;
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { buildConversationListCursorFromRecord } from '@appthen/sdk-core';
2
3
  export { ChatHubMessageTimeline } from './ChatHubMessageTimeline';
3
4
  export { ChatHubComposer } from './ChatHubComposer';
4
5
  export { ChatHubMessageActions } from './ChatHubMessageActions';
@@ -6,6 +7,17 @@ export { ChatHubMessageBody, } from './ChatHubMessageBody';
6
7
  export { createChatHubDefaultBodyRenderers, } from './ChatHubDefaultBodyRenderers';
7
8
  export { extractChatHubMessagePlainText, useChatHubMessageMutations, } from './useChatHubMessageMutations';
8
9
  export { useChatHubMessageActions, } from './useChatHubMessageActions';
10
+ /**
11
+ * refreshMessages 未显式给出 limit 时的兜底页大小。
12
+ *
13
+ * 「不传 limit = 拉整段会话历史」是个陷阱:调用方少写一个参数,服务端就会把
14
+ * 该会话的全部消息一次性返回(2000 条消息的会话 ≈ 4.5MB/次),在 30s 兜底轮询
15
+ * 下尤其致命。任何「刷新消息」语义的调用都不需要全量,故在此兜底;确需历史请走
16
+ * refreshMessagesPage 的 beforeCreatedAt 分页。
17
+ */
18
+ const DEFAULT_MESSAGE_REFRESH_LIMIT = 50;
19
+ /** 会话列表「加载更多」的默认页大小(与服务端 limit 上限 200 无冲突)。 */
20
+ export const DEFAULT_CONVERSATION_PAGE_SIZE = 30;
9
21
  export function areConversationMessagesEqual(left, right) {
10
22
  return JSON.stringify(left) === JSON.stringify(right);
11
23
  }
@@ -88,6 +100,8 @@ export function useChatHubApp(options) {
88
100
  const [error, setError] = useState(null);
89
101
  const [busy, setBusy] = useState(false);
90
102
  const [conversations, setConversations] = useState([]);
103
+ // ref 镜像:loadMoreConversations 需要「当前最后一条」构造游标,避免闭包过期
104
+ const conversationsRef = useRef([]);
91
105
  const [messages, setMessages] = useState([]);
92
106
  const [selectedConversationId, setSelectedConversationIdState] = useState(initialConversationId);
93
107
  const selectedConversationIdRef = useRef(initialConversationId);
@@ -145,12 +159,16 @@ export function useChatHubApp(options) {
145
159
  setBusy(false);
146
160
  }
147
161
  }, []);
148
- const refreshConversations = useCallback(async () => {
162
+ const refreshConversations = useCallback(async (options) => {
149
163
  const requestSeq = ++refreshConversationsRequestSeqRef.current;
150
- const list = await runtime.listConversations(selectedSenderIdRef.current);
164
+ const list = await runtime.listConversations(selectedSenderIdRef.current, {
165
+ ...(options?.limit ? { limit: options.limit } : {}),
166
+ ...(options?.search ? { search: options.search } : {}),
167
+ });
151
168
  if (requestSeq !== refreshConversationsRequestSeqRef.current) {
152
169
  return;
153
170
  }
171
+ conversationsRef.current = list;
154
172
  setConversations(list);
155
173
  setSelectedConversationIdState((current) => {
156
174
  const next = current || list[0]?.conversationId || '';
@@ -158,6 +176,32 @@ export function useChatHubApp(options) {
158
176
  return next;
159
177
  });
160
178
  }, [runtime]);
179
+ /**
180
+ * 加载下一页(P1 游标分页):以当前列表最后一条构造 keyset 游标向后翻页,
181
+ * 追加去重(按 conversationId)。返回新增条数;0 = 已到底(调用方可收起入口)。
182
+ */
183
+ const loadMoreConversations = useCallback(async (limit = DEFAULT_CONVERSATION_PAGE_SIZE) => {
184
+ const current = conversationsRef.current;
185
+ const cursor = buildConversationListCursorFromRecord(current[current.length - 1]);
186
+ if (!cursor) {
187
+ return 0;
188
+ }
189
+ const next = await runtime.listConversations(selectedSenderIdRef.current, {
190
+ limit,
191
+ cursor,
192
+ });
193
+ if (next.length === 0) {
194
+ return 0;
195
+ }
196
+ const existingIds = new Set(current.map((item) => item.conversationId));
197
+ const appended = next.filter((item) => !existingIds.has(item.conversationId));
198
+ if (appended.length > 0) {
199
+ const merged = [...current, ...appended];
200
+ conversationsRef.current = merged;
201
+ setConversations(merged);
202
+ }
203
+ return appended.length;
204
+ }, [runtime]);
161
205
  const refreshMessages = useCallback(async (conversationIdOrOptions) => {
162
206
  const options = typeof conversationIdOrOptions === 'string'
163
207
  ? {
@@ -175,7 +219,7 @@ export function useChatHubApp(options) {
175
219
  }
176
220
  const list = await runtime.listMessages({
177
221
  conversationId: targetConversationId,
178
- limit: options.limit,
222
+ limit: options.limit ?? DEFAULT_MESSAGE_REFRESH_LIMIT,
179
223
  beforeCreatedAt: options.beforeCreatedAt,
180
224
  afterSequence: options.afterSequence,
181
225
  });
@@ -297,6 +341,7 @@ export function useChatHubApp(options) {
297
341
  setSelectedSenderId,
298
342
  bootstrapConversation,
299
343
  refreshConversations,
344
+ loadMoreConversations,
300
345
  refreshMessages,
301
346
  refreshMessagesPage,
302
347
  mergeMessages,
@@ -309,6 +354,7 @@ export function useChatHubApp(options) {
309
354
  config,
310
355
  conversations,
311
356
  error,
357
+ loadMoreConversations,
312
358
  messages,
313
359
  refreshConversations,
314
360
  refreshMessages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appthen/sdk-react",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "AppThen Conversation Hub SDK - React headless hooks",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,7 +17,7 @@
17
17
  "README.md"
18
18
  ],
19
19
  "dependencies": {
20
- "@appthen/sdk-core": "0.0.2"
20
+ "@appthen/sdk-core": "0.0.3"
21
21
  },
22
22
  "peerDependencies": {
23
23
  "react": "^18.0.0 || ^19.0.0"