@smartspace/chat-ui 1.13.1-dev.aa44752 → 1.13.1-dev.b2f9256

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.ts CHANGED
@@ -238,6 +238,14 @@ type MessageThread = {
238
238
  totalMessages: number;
239
239
  pinned: boolean;
240
240
  workSpaceId: string;
241
+ /**
242
+ * Monotonic version of when this summary was emitted (epoch ms). Used by
243
+ * `applyThreadToCache` to reject stale writes — e.g. a SignalR summary
244
+ * landing after a fresher SSE thread frame because the server's DB write
245
+ * lagged Redis. Mappers derive this from `lastUpdatedAt`; client-side
246
+ * writers like `ensureDraftThread` use `Date.now()`.
247
+ */
248
+ summaryEmittedAt: number;
241
249
  };
242
250
  type ThreadsResponse = {
243
251
  data: MessageThread[];
@@ -969,20 +977,6 @@ declare function mapMentionUserDtoToModel(dto: MentionUserDto): MentionUser;
969
977
  declare function mapWorkspaceDtoToModel(dto: WorkspaceDto): Workspace;
970
978
  declare const mapWorkspacesDtoToModels: (arr: WorkspacesListItemDto[]) => Workspace[];
971
979
 
972
- /**
973
- * Write a freshly-observed thread (from SignalR or an SSE thread frame)
974
- * directly into the relevant query caches so subscribers paint without a
975
- * refetch roundtrip.
976
- *
977
- * - Merges into `threadsKeys.detail(workspaceId, thread.id)`.
978
- * - Splices into every threads-list cache for the workspace, handling both
979
- * finite `ThreadsResponse` and infinite `{ pages, pageParams }` shapes.
980
- *
981
- * Returns `true` when the thread was found in at least one list cache.
982
- * Callers that need to surface brand-new threads (e.g. another user just
983
- * created one) can fall back to invalidating the list queries when this
984
- * returns `false`.
985
- */
986
980
  declare function applyThreadToCache(qc: QueryClient, thread: MessageThread): boolean;
987
981
  /**
988
982
  * Invalidate every threads-list cache for a workspace. Use as a fallback when
package/dist/index.js CHANGED
@@ -3233,7 +3233,20 @@ var threadsKeys = {
3233
3233
  };
3234
3234
 
3235
3235
  // src/domains/threads/cache.ts
3236
+ function isStaleSummary(incoming, existing) {
3237
+ if (!existing) return false;
3238
+ if (typeof existing.summaryEmittedAt !== "number") return false;
3239
+ if (typeof incoming.summaryEmittedAt !== "number") return false;
3240
+ if (incoming.summaryEmittedAt >= existing.summaryEmittedAt) return false;
3241
+ return existing.isFlowRunning === false && incoming.isFlowRunning === true;
3242
+ }
3236
3243
  function applyThreadToCache(qc, thread) {
3244
+ const existingDetail = qc.getQueryData(
3245
+ threadsKeys.detail(thread.workSpaceId, thread.id)
3246
+ );
3247
+ if (isStaleSummary(thread, existingDetail)) {
3248
+ return false;
3249
+ }
3237
3250
  qc.setQueryData(
3238
3251
  threadsKeys.detail(thread.workSpaceId, thread.id),
3239
3252
  (old) => ({ ...old ?? thread, ...thread })
@@ -3254,6 +3267,7 @@ function applyThreadToCache(qc, thread) {
3254
3267
  if (!page?.data) return page;
3255
3268
  const idx2 = page.data.findIndex((t) => t.id === thread.id);
3256
3269
  if (idx2 === -1) return page;
3270
+ if (isStaleSummary(thread, page.data[idx2])) return page;
3257
3271
  changed = true;
3258
3272
  foundInList = true;
3259
3273
  const nextData2 = page.data.slice();
@@ -3266,6 +3280,7 @@ function applyThreadToCache(qc, thread) {
3266
3280
  if (!list2.data) return old;
3267
3281
  const idx = list2.data.findIndex((t) => t.id === thread.id);
3268
3282
  if (idx === -1) return old;
3283
+ if (isStaleSummary(thread, list2.data[idx])) return old;
3269
3284
  foundInList = true;
3270
3285
  const nextData = list2.data.slice();
3271
3286
  nextData[idx] = { ...nextData[idx], ...thread };
@@ -3335,36 +3350,40 @@ var {
3335
3350
  messageThreadsGetMessageThreadWorkspacesWorkspaceIdMessagethreadsIdResponse: threadResponseSchema
3336
3351
  } = ChatZod;
3337
3352
  function mapThreadDtoToModel(dto) {
3353
+ const lastUpdatedAt = utcDate(dto.lastUpdatedAt);
3338
3354
  return {
3339
3355
  id: dto.id,
3340
3356
  createdAt: utcDate(dto.createdAt),
3341
3357
  createdBy: dto.createdBy ?? "",
3342
3358
  createdByUserId: dto.createdByUserId,
3343
3359
  isFlowRunning: dto.isFlowRunning,
3344
- lastUpdatedAt: utcDate(dto.lastUpdatedAt),
3360
+ lastUpdatedAt,
3345
3361
  lastUpdatedByUserId: dto.lastUpdatedByUserId,
3346
3362
  name: dto.name ?? "",
3347
3363
  totalMessages: dto.totalMessages,
3348
3364
  pinned: dto.favorited,
3349
- workSpaceId: dto.workSpaceId
3365
+ workSpaceId: dto.workSpaceId,
3366
+ summaryEmittedAt: lastUpdatedAt.getTime()
3350
3367
  };
3351
3368
  }
3352
3369
  function mapThreadsResponseDtoToModel(dto) {
3353
3370
  return { data: dto.data.map(mapThreadDtoToModel), total: dto.total };
3354
3371
  }
3355
3372
  function mapSignalRThreadSummaryToModel(summary) {
3373
+ const lastUpdatedAt = utcDate(summary.lastUpdatedAt);
3356
3374
  return {
3357
3375
  id: summary.id,
3358
3376
  createdAt: utcDate(summary.createdAt),
3359
3377
  createdBy: summary.createdBy ?? "",
3360
3378
  createdByUserId: summary.createdByUserId,
3361
3379
  isFlowRunning: summary.isFlowRunning,
3362
- lastUpdatedAt: utcDate(summary.lastUpdatedAt),
3380
+ lastUpdatedAt,
3363
3381
  lastUpdatedByUserId: summary.lastUpdatedByUserId,
3364
3382
  name: summary.name ?? "",
3365
3383
  totalMessages: summary.totalMessages,
3366
3384
  pinned: summary.favorited,
3367
- workSpaceId: summary.workSpaceId
3385
+ workSpaceId: summary.workSpaceId,
3386
+ summaryEmittedAt: lastUpdatedAt.getTime()
3368
3387
  };
3369
3388
  }
3370
3389
  var threadDetailOptions = ({
@@ -3457,6 +3476,15 @@ var messagesMutationsKeys = {
3457
3476
  };
3458
3477
 
3459
3478
  // src/domains/messages/mutations.ts
3479
+ function reconcileWithMessage(old, incoming, onDuplicate = "keep-existing") {
3480
+ const stable = old.filter((m) => !m.optimistic);
3481
+ const idx = stable.findIndex((m) => m.id === incoming.id);
3482
+ if (idx === -1) return [...stable, incoming];
3483
+ if (onDuplicate === "keep-existing") return stable;
3484
+ const copy = stable.slice();
3485
+ copy[idx] = incoming;
3486
+ return copy;
3487
+ }
3460
3488
  function useSendMessage() {
3461
3489
  const qc = useQueryClient();
3462
3490
  const { userId, displayName: userName } = useChatIdentity();
@@ -3540,7 +3568,10 @@ function useSendMessage() {
3540
3568
  toast.error("There was an error posting your message");
3541
3569
  throw err;
3542
3570
  }
3543
- qc.setQueryData(messagesKeys.list(threadId), [realMessage]);
3571
+ qc.setQueryData(
3572
+ messagesKeys.list(threadId),
3573
+ (old = []) => reconcileWithMessage(old, realMessage)
3574
+ );
3544
3575
  qc.setQueryData(
3545
3576
  threadsKeys.detail(workspaceId, threadId),
3546
3577
  (old) => old ? { ...old, isFlowRunning: true } : old
@@ -3596,14 +3627,10 @@ function useAddInputToMessage() {
3596
3627
  });
3597
3628
  },
3598
3629
  onSuccess: (message, { threadId }) => {
3599
- qc.setQueryData(messagesKeys.list(threadId), (old = []) => {
3600
- const stable = old.filter((x) => !x.optimistic);
3601
- const idx = stable.findIndex((x) => x.id === message.id);
3602
- if (idx === -1) return [...stable, message];
3603
- const copy = stable.slice();
3604
- copy[idx] = message;
3605
- return copy;
3606
- });
3630
+ qc.setQueryData(
3631
+ messagesKeys.list(threadId),
3632
+ (old = []) => reconcileWithMessage(old, message, "replace")
3633
+ );
3607
3634
  },
3608
3635
  onError: (_e, { threadId }) => {
3609
3636
  qc.setQueryData(
@@ -19804,6 +19831,10 @@ function MessageList({
19804
19831
  const messagesEndRef = useRef(null);
19805
19832
  const prevMessageCountRef = useRef(0);
19806
19833
  const hasInitialScrollRef = useRef(false);
19834
+ const everHadMessagesRef = useRef({
19835
+ threadId: "",
19836
+ had: false
19837
+ });
19807
19838
  const isMobile = useIsMobile();
19808
19839
  const { data: activeWorkspace } = useWorkspace(workspaceId);
19809
19840
  const [isAtBottom, setIsAtBottom] = useState(true);
@@ -19875,8 +19906,15 @@ function MessageList({
19875
19906
  ro.observe(content);
19876
19907
  return () => ro.disconnect();
19877
19908
  }, [isAtBottom, scrollToBottom]);
19909
+ const safeMessages = messages ?? [];
19910
+ if (everHadMessagesRef.current.threadId !== threadId) {
19911
+ everHadMessagesRef.current = { threadId, had: safeMessages.length > 0 };
19912
+ } else if (safeMessages.length > 0) {
19913
+ everHadMessagesRef.current.had = true;
19914
+ }
19915
+ const hadMessagesBefore = everHadMessagesRef.current.had;
19878
19916
  const isLoading = isChoosingThread || (threadPending || threadFetching) && !thread || (messagesPending || messagesFetching) && messages === void 0;
19879
- if (isLoading) {
19917
+ if (isLoading && !hadMessagesBefore) {
19880
19918
  return /* @__PURE__ */ jsx(
19881
19919
  "div",
19882
19920
  {
@@ -19892,7 +19930,7 @@ function MessageList({
19892
19930
  }
19893
19931
  );
19894
19932
  }
19895
- if (threadError || messagesError) {
19933
+ if ((threadError || messagesError) && !hadMessagesBefore) {
19896
19934
  return /* @__PURE__ */ jsx("div", { className: "flex flex-1 items-center justify-center p-6", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-md space-y-3", children: [
19897
19935
  threadError && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-destructive", children: [
19898
19936
  /* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4" }),
@@ -19904,8 +19942,7 @@ function MessageList({
19904
19942
  ] })
19905
19943
  ] }) });
19906
19944
  }
19907
- const safeMessages = messages ?? [];
19908
- if (safeMessages.length === 0) {
19945
+ if (safeMessages.length === 0 && !hadMessagesBefore) {
19909
19946
  return /* @__PURE__ */ jsxs("div", { className: "flex overflow-auto flex-shrink-10 flex-col p-8 text-center", children: [
19910
19947
  /* @__PURE__ */ jsx("h3", { className: "text-lg font-medium mb-2", children: activeWorkspace?.name ?? "No messages yet" }),
19911
19948
  activeWorkspace?.firstPrompt && /* @__PURE__ */ jsx("div", { className: "max-w-3xl mx-auto p-4", children: /* @__PURE__ */ jsx(MessageMarkdown, { value: activeWorkspace.firstPrompt }) })