@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 +21 -5
- package/dist/index.d.ts +21 -5
- package/dist/index.js +135 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +135 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
/**
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
/**
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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 =
|
|
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
|
-
}, [
|
|
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);
|