@anakin824/prdg-chat-ui 0.1.0 → 0.2.1

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.
package/dist/index.d.mts CHANGED
@@ -134,10 +134,16 @@ type ChatProviderProps = {
134
134
  apiUrl: string;
135
135
  /** Current bearer token (HDS JWT in integrated mode, prdg-chat JWT in standalone). */
136
136
  token: string;
137
- /** prdg-chat user UUID — identifies the current user for own-message display, mentions, etc. */
138
- userId: string;
139
- /** Internal prdg-chat tenant UUID (JWT / API). */
140
- tenantId: string;
137
+ /**
138
+ * prdg-chat user UUID — identifies the current user for own-message display, mentions, etc.
139
+ * When omitted, ChatProvider calls GET /me with the supplied token to resolve it automatically.
140
+ */
141
+ userId?: string;
142
+ /**
143
+ * Internal prdg-chat tenant UUID (JWT / API).
144
+ * When omitted, resolved automatically via GET /me.
145
+ */
146
+ tenantId?: string;
141
147
  /**
142
148
  * Conduitly `tenant_id` for NATS (`chat.{conduitly_tenant_id}.user.*`). Set via `conduitly_tenant_id` in dev.json or `NEXT_PUBLIC_CONDUITLY_TENANT_ID`; do not rely on internal prdg-chat tenant UUID.
143
149
  */
@@ -163,9 +169,14 @@ type ChatProviderProps = {
163
169
  natsWsUrl?: string;
164
170
  /** Optional NATS auth token (when the server uses token authentication). */
165
171
  natsToken?: string;
172
+ /**
173
+ * When true, logs `[prdg-chat]` diagnostics (identity resolution, message `sender_id` vs context `userId`).
174
+ * If omitted, set env `NEXT_PUBLIC_PRDG_CHAT_DEBUG=true` in the host app instead.
175
+ */
176
+ debug?: boolean;
166
177
  children: ReactNode;
167
178
  };
168
- declare function ChatProvider({ apiUrl, token, userId, tenantId, conduitlyTenantId, theme, pollIntervalMs, onUnreadChange: _onUnreadChange, onTokenRefresh, callEnabled, natsWsUrl, natsToken, children, }: ChatProviderProps): react.JSX.Element;
179
+ declare function ChatProvider({ apiUrl, token, userId: userIdProp, tenantId: tenantIdProp, conduitlyTenantId: conduitlyTenantIdProp, theme, pollIntervalMs, onUnreadChange: _onUnreadChange, onTokenRefresh, callEnabled, natsWsUrl, natsToken, debug: debugProp, children, }: ChatProviderProps): react.JSX.Element | null;
169
180
 
170
181
  type ListConversationsRes = {
171
182
  items: Conversation[];
@@ -210,6 +221,9 @@ declare class ChatAPI {
210
221
  onAuthError?: (() => Promise<string | null>) | undefined);
211
222
  private headers;
212
223
  private json;
224
+ getMe(): Promise<AppUser & {
225
+ conduitly_tenant_id?: string;
226
+ }>;
213
227
  listConversations(cursor?: string, limit?: number): Promise<ListConversationsRes>;
214
228
  listMessages(conversationId: string, cursor?: string, limit?: number): Promise<ListMessagesRes>;
215
229
  findOrCreateDirect(peerUserId: string): Promise<Conversation>;
@@ -265,6 +279,8 @@ type ChatContextValue = {
265
279
  * Key: conversationId. Value: array of user IDs (never includes the current user).
266
280
  */
267
281
  typingByConversation: Record<string, string[]>;
282
+ /** When true, logs identity + message ownership diagnostics to the console. */
283
+ debug: boolean;
268
284
  };
269
285
  declare function useChat(): ChatContextValue;
270
286
 
package/dist/index.d.ts CHANGED
@@ -134,10 +134,16 @@ type ChatProviderProps = {
134
134
  apiUrl: string;
135
135
  /** Current bearer token (HDS JWT in integrated mode, prdg-chat JWT in standalone). */
136
136
  token: string;
137
- /** prdg-chat user UUID — identifies the current user for own-message display, mentions, etc. */
138
- userId: string;
139
- /** Internal prdg-chat tenant UUID (JWT / API). */
140
- tenantId: string;
137
+ /**
138
+ * prdg-chat user UUID — identifies the current user for own-message display, mentions, etc.
139
+ * When omitted, ChatProvider calls GET /me with the supplied token to resolve it automatically.
140
+ */
141
+ userId?: string;
142
+ /**
143
+ * Internal prdg-chat tenant UUID (JWT / API).
144
+ * When omitted, resolved automatically via GET /me.
145
+ */
146
+ tenantId?: string;
141
147
  /**
142
148
  * Conduitly `tenant_id` for NATS (`chat.{conduitly_tenant_id}.user.*`). Set via `conduitly_tenant_id` in dev.json or `NEXT_PUBLIC_CONDUITLY_TENANT_ID`; do not rely on internal prdg-chat tenant UUID.
143
149
  */
@@ -163,9 +169,14 @@ type ChatProviderProps = {
163
169
  natsWsUrl?: string;
164
170
  /** Optional NATS auth token (when the server uses token authentication). */
165
171
  natsToken?: string;
172
+ /**
173
+ * When true, logs `[prdg-chat]` diagnostics (identity resolution, message `sender_id` vs context `userId`).
174
+ * If omitted, set env `NEXT_PUBLIC_PRDG_CHAT_DEBUG=true` in the host app instead.
175
+ */
176
+ debug?: boolean;
166
177
  children: ReactNode;
167
178
  };
168
- declare function ChatProvider({ apiUrl, token, userId, tenantId, conduitlyTenantId, theme, pollIntervalMs, onUnreadChange: _onUnreadChange, onTokenRefresh, callEnabled, natsWsUrl, natsToken, children, }: ChatProviderProps): react.JSX.Element;
179
+ declare function ChatProvider({ apiUrl, token, userId: userIdProp, tenantId: tenantIdProp, conduitlyTenantId: conduitlyTenantIdProp, theme, pollIntervalMs, onUnreadChange: _onUnreadChange, onTokenRefresh, callEnabled, natsWsUrl, natsToken, debug: debugProp, children, }: ChatProviderProps): react.JSX.Element | null;
169
180
 
170
181
  type ListConversationsRes = {
171
182
  items: Conversation[];
@@ -210,6 +221,9 @@ declare class ChatAPI {
210
221
  onAuthError?: (() => Promise<string | null>) | undefined);
211
222
  private headers;
212
223
  private json;
224
+ getMe(): Promise<AppUser & {
225
+ conduitly_tenant_id?: string;
226
+ }>;
213
227
  listConversations(cursor?: string, limit?: number): Promise<ListConversationsRes>;
214
228
  listMessages(conversationId: string, cursor?: string, limit?: number): Promise<ListMessagesRes>;
215
229
  findOrCreateDirect(peerUserId: string): Promise<Conversation>;
@@ -265,6 +279,8 @@ type ChatContextValue = {
265
279
  * Key: conversationId. Value: array of user IDs (never includes the current user).
266
280
  */
267
281
  typingByConversation: Record<string, string[]>;
282
+ /** When true, logs identity + message ownership diagnostics to the console. */
283
+ debug: boolean;
268
284
  };
269
285
  declare function useChat(): ChatContextValue;
270
286
 
package/dist/index.js CHANGED
@@ -51,6 +51,9 @@ var ChatAPI = class {
51
51
  }
52
52
  return res.json();
53
53
  }
54
+ getMe() {
55
+ return this.json("/me");
56
+ }
54
57
  listConversations(cursor, limit = 50) {
55
58
  const q = new URLSearchParams();
56
59
  if (cursor) q.set("cursor", cursor);
@@ -115,6 +118,25 @@ var ChatAPI = class {
115
118
  }
116
119
  };
117
120
 
121
+ // src/chat/lib/chatDebugLog.ts
122
+ function resolveChatDebug(debugProp) {
123
+ if (debugProp === true) return true;
124
+ if (debugProp === false) return false;
125
+ try {
126
+ return typeof process !== "undefined" && process.env?.NEXT_PUBLIC_PRDG_CHAT_DEBUG === "true";
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+ function chatDebugLog(debug, ...args) {
132
+ if (!debug) return;
133
+ console.log("[prdg-chat]", ...args);
134
+ }
135
+ function chatDebugWarn(debug, ...args) {
136
+ if (!debug) return;
137
+ console.warn("[prdg-chat]", ...args);
138
+ }
139
+
118
140
  // src/chat/lib/queryKeys.ts
119
141
  var chatKeys = {
120
142
  all: ["chat"],
@@ -324,9 +346,9 @@ var defaultTheme = {
324
346
  function ChatProvider({
325
347
  apiUrl,
326
348
  token,
327
- userId,
328
- tenantId,
329
- conduitlyTenantId,
349
+ userId: userIdProp,
350
+ tenantId: tenantIdProp,
351
+ conduitlyTenantId: conduitlyTenantIdProp,
330
352
  theme,
331
353
  pollIntervalMs = 3e4,
332
354
  onUnreadChange: _onUnreadChange,
@@ -334,8 +356,10 @@ function ChatProvider({
334
356
  callEnabled = false,
335
357
  natsWsUrl,
336
358
  natsToken,
359
+ debug: debugProp,
337
360
  children
338
361
  }) {
362
+ const debug = resolveChatDebug(debugProp);
339
363
  const queryClient = react.useMemo(
340
364
  () => new reactQuery.QueryClient({
341
365
  defaultOptions: {
@@ -358,6 +382,62 @@ function ChatProvider({
358
382
  () => new ChatAPI(apiUrl.replace(/\/$/, ""), () => tokenRef.current, handleAuthError),
359
383
  [apiUrl, handleAuthError]
360
384
  );
385
+ const [resolvedUserId, setResolvedUserId] = react.useState(userIdProp ?? null);
386
+ const [resolvedTenantId, setResolvedTenantId] = react.useState(tenantIdProp ?? null);
387
+ const [resolvedConduitlyTenantId, setResolvedConduitlyTenantId] = react.useState(
388
+ conduitlyTenantIdProp
389
+ );
390
+ const [meError, setMeError] = react.useState(null);
391
+ react.useEffect(() => {
392
+ chatDebugLog(debug, "ChatProvider: session inputs", {
393
+ apiUrl: apiUrl.replace(/\/$/, ""),
394
+ hasToken: Boolean(token?.trim?.()),
395
+ userIdProp: userIdProp ?? null,
396
+ tenantIdProp: tenantIdProp ?? null,
397
+ conduitlyTenantIdProp: conduitlyTenantIdProp ?? null
398
+ });
399
+ }, [debug, apiUrl, token, userIdProp, tenantIdProp, conduitlyTenantIdProp]);
400
+ react.useEffect(() => {
401
+ setMeError(null);
402
+ if (userIdProp) setResolvedUserId(userIdProp);
403
+ if (tenantIdProp) setResolvedTenantId(tenantIdProp);
404
+ if (conduitlyTenantIdProp) setResolvedConduitlyTenantId(conduitlyTenantIdProp);
405
+ if (userIdProp && tenantIdProp) {
406
+ chatDebugLog(debug, "identity: using userId + tenantId props (skipping GET /me)", {
407
+ userId: userIdProp,
408
+ tenantId: tenantIdProp
409
+ });
410
+ return;
411
+ }
412
+ chatDebugLog(debug, "identity: fetching GET /me \u2026", {
413
+ missingUserId: !userIdProp,
414
+ missingTenantId: !tenantIdProp
415
+ });
416
+ let cancelled = false;
417
+ api.getMe().then((me) => {
418
+ if (cancelled) return;
419
+ chatDebugLog(debug, "identity: GET /me ok", {
420
+ id: me.id,
421
+ tenant_id: me.tenant_id,
422
+ display_name: me.display_name,
423
+ ext_user_id: me.ext_user_id ?? null,
424
+ conduitly_tenant_id: me.conduitly_tenant_id ?? null
425
+ });
426
+ if (!userIdProp) setResolvedUserId(me.id);
427
+ if (!tenantIdProp) setResolvedTenantId(me.tenant_id);
428
+ if (!conduitlyTenantIdProp && me.conduitly_tenant_id) {
429
+ setResolvedConduitlyTenantId(me.conduitly_tenant_id);
430
+ }
431
+ }).catch((err) => {
432
+ if (!cancelled) setMeError(err instanceof Error ? err.message : String(err));
433
+ chatDebugWarn(debug, "identity: GET /me failed", err);
434
+ });
435
+ return () => {
436
+ cancelled = true;
437
+ };
438
+ }, [api, token, userIdProp, tenantIdProp, conduitlyTenantIdProp, debug]);
439
+ const userId = resolvedUserId ?? "";
440
+ const tenantId = resolvedTenantId ?? "";
361
441
  const mergedTheme = react.useMemo(() => ({ ...defaultTheme, ...theme }), [theme]);
362
442
  const styleTag = react.useMemo(() => {
363
443
  const t = mergedTheme;
@@ -379,7 +459,7 @@ function ChatProvider({
379
459
  const natsUrl = natsWsUrl?.trim() ?? "";
380
460
  const wsConnected = Boolean(natsUrl) && natsConnected;
381
461
  const natsTenantId = react.useMemo(() => {
382
- const c = conduitlyTenantId?.trim();
462
+ const c = resolvedConduitlyTenantId?.trim();
383
463
  if (c) return c;
384
464
  if (process.env.NODE_ENV === "development" && natsUrl) {
385
465
  console.warn(
@@ -387,7 +467,7 @@ function ChatProvider({
387
467
  );
388
468
  }
389
469
  return tenantId.trim();
390
- }, [conduitlyTenantId, tenantId, natsUrl]);
470
+ }, [resolvedConduitlyTenantId, tenantId, natsUrl]);
391
471
  const typingByConversation = react.useMemo(() => ({}), []);
392
472
  const sendTyping = react.useCallback((_conversationId) => {
393
473
  }, []);
@@ -419,7 +499,8 @@ function ChatProvider({
419
499
  sendTyping,
420
500
  sendTypingStop,
421
501
  sendRead,
422
- typingByConversation
502
+ typingByConversation,
503
+ debug
423
504
  }),
424
505
  [
425
506
  api,
@@ -433,9 +514,18 @@ function ChatProvider({
433
514
  sendTyping,
434
515
  sendTypingStop,
435
516
  sendRead,
436
- typingByConversation
517
+ typingByConversation,
518
+ debug
437
519
  ]
438
520
  );
521
+ react.useEffect(() => {
522
+ if (!resolvedUserId || !resolvedTenantId) return;
523
+ chatDebugLog(debug, "ChatProvider: context ready", {
524
+ userId: resolvedUserId,
525
+ tenantId: resolvedTenantId,
526
+ natsTenantId
527
+ });
528
+ }, [debug, resolvedUserId, resolvedTenantId, natsTenantId]);
439
529
  const layoutStyle = {
440
530
  flex: 1,
441
531
  minHeight: 0,
@@ -443,6 +533,12 @@ function ChatProvider({
443
533
  flexDirection: "column",
444
534
  boxSizing: "border-box"
445
535
  };
536
+ if (!resolvedUserId || !resolvedTenantId) {
537
+ if (meError) {
538
+ return /* @__PURE__ */ React.createElement("div", { style: { padding: 16, fontFamily: "system-ui, sans-serif", color: "#b91c1c", fontSize: 13 } }, "Chat failed to load: ", meError);
539
+ }
540
+ return null;
541
+ }
446
542
  return /* @__PURE__ */ React.createElement(reactQuery.QueryClientProvider, { client: queryClient }, /* @__PURE__ */ React.createElement(ChatContext.Provider, { value }, /* @__PURE__ */ React.createElement("div", { style: layoutStyle }, natsUrl ? /* @__PURE__ */ React.createElement(ChatNatsBridge, { natsWsUrl: natsUrl, natsToken, onConnectedChange: setNatsConnected }) : null, /* @__PURE__ */ React.createElement("style", { dangerouslySetInnerHTML: { __html: styleTag } }), /* @__PURE__ */ React.createElement("div", { "data-chat-root": true, style: { ...layoutStyle, flex: 1 } }, children))));
447
543
  }
448
544
  function useChatActions() {
@@ -777,7 +873,7 @@ function escapeReg(s) {
777
873
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
778
874
  }
779
875
  function MessageInput({ conversationId }) {
780
- const { api, sendTyping, sendTypingStop, userId } = useChat();
876
+ const { api, sendTyping, sendTypingStop, userId, debug } = useChat();
781
877
  const qc = reactQuery.useQueryClient();
782
878
  const { uploadFile } = useUpload();
783
879
  const { recording, start, stop } = useVoiceRecorder();
@@ -917,6 +1013,17 @@ function MessageInput({ conversationId }) {
917
1013
  });
918
1014
  },
919
1015
  onSuccess: (confirmedMessage, _payload, context) => {
1016
+ if (debug && confirmedMessage.sender_id !== userId) {
1017
+ chatDebugWarn(
1018
+ debug,
1019
+ "MessageInput: confirmed sender_id !== context userId (own-bubble alignment will be wrong)",
1020
+ {
1021
+ contextUserId: userId,
1022
+ serverSenderId: confirmedMessage.sender_id,
1023
+ messageId: confirmedMessage.id
1024
+ }
1025
+ );
1026
+ }
920
1027
  qc.setQueryData(chatKeys.messages(conversationId), (old) => {
921
1028
  if (!old) return old;
922
1029
  const optimisticIdx = old.items.findIndex((m) => m.id === context?.optimisticId);
@@ -1259,10 +1366,30 @@ function MessageThread({
1259
1366
  isFetchingOlder,
1260
1367
  hasMoreMessages
1261
1368
  }) {
1369
+ const { userId, debug } = useChat();
1262
1370
  const scrollRef = react.useRef(null);
1263
1371
  const bottomRef = react.useRef(null);
1264
1372
  const sentinelRef = react.useRef(null);
1265
1373
  const ordered = [...messages].reverse();
1374
+ react.useEffect(() => {
1375
+ if (!debug || messages.length === 0) return;
1376
+ const senders = new Set(messages.map((m) => m.sender_id));
1377
+ const ownCount = messages.filter((m) => m.sender_id === userId).length;
1378
+ const mismatchSample = messages.find((m) => m.sender_id !== userId);
1379
+ chatDebugLog(debug, "MessageThread: sender snapshot", {
1380
+ messageCount: messages.length,
1381
+ contextUserId: userId,
1382
+ uniqueSenderIds: [...senders],
1383
+ ownMessageCount: ownCount,
1384
+ contextMatchesAnySender: userId ? senders.has(userId) : false
1385
+ });
1386
+ if (userId && ownCount === 0 && messages.length > 0) {
1387
+ chatDebugWarn(debug, "MessageThread: no messages match context userId (all bubbles show as \u201Cother\u201D)", {
1388
+ contextUserId: userId,
1389
+ sampleSenderId: mismatchSample?.sender_id ?? null
1390
+ });
1391
+ }
1392
+ }, [debug, messages, userId]);
1266
1393
  const seenIdsRef = react.useRef(/* @__PURE__ */ new Set());
1267
1394
  const initializedRef = react.useRef(false);
1268
1395
  const prevCountRef = react.useRef(0);