@smartspace/chat-ui 1.13.1-pr.258.6c80b75 → 1.13.1-pr.260.2e65c1d

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
@@ -4,7 +4,7 @@ import { Loader2, Check, X, Paperclip, ArrowBigUp, Minimize2, AlertTriangle, Fil
4
4
  import * as React8 from 'react';
5
5
  import { createContext, forwardRef, useImperativeHandle, useRef, useState, useEffect, useMemo, useCallback, createElement, useContext } from 'react';
6
6
  import { createPortal } from 'react-dom';
7
- import { useQuery, queryOptions, useQueryClient, useMutation } from '@tanstack/react-query';
7
+ import { useQuery, queryOptions, useQueryClient, useMutation, skipToken } from '@tanstack/react-query';
8
8
  import { toast } from 'sonner';
9
9
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
10
10
  import { Editor, rootCtx, defaultValueCtx, editorViewOptionsCtx, editorViewCtx, serializerCtx, SchemaReady, nodeViewCtx, markViewCtx, schemaCtx, prosePluginsCtx, nodesCtx } from '@milkdown/core';
@@ -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 = ({
@@ -3418,11 +3437,12 @@ var useThread = ({
3418
3437
  });
3419
3438
  };
3420
3439
  var useThreadIsRunning = (workspaceId, threadId) => {
3421
- const { data: thread } = useThread({
3422
- workspaceId: workspaceId ?? "",
3423
- threadId: threadId ?? "",
3424
- enabled: !!workspaceId && !!threadId
3440
+ const queryClient = useQueryClient();
3441
+ const { data: detailThread } = useQuery({
3442
+ queryKey: threadsKeys.detail(workspaceId ?? "", threadId ?? ""),
3443
+ queryFn: skipToken
3425
3444
  });
3445
+ const listThread = workspaceId && threadId ? getThreadPlaceholderFromListCache(queryClient, workspaceId, threadId) : void 0;
3426
3446
  const { data: optimistic } = useQuery({
3427
3447
  queryKey: threadsKeys.optimisticRunning(threadId ?? ""),
3428
3448
  queryFn: () => false,
@@ -3430,7 +3450,7 @@ var useThreadIsRunning = (workspaceId, threadId) => {
3430
3450
  staleTime: Infinity,
3431
3451
  enabled: !!threadId
3432
3452
  });
3433
- return !!optimistic || !!thread?.isFlowRunning;
3453
+ return !!optimistic || !!(detailThread ?? listThread)?.isFlowRunning;
3434
3454
  };
3435
3455
 
3436
3456
  // src/domains/messages/enums.ts
@@ -3539,7 +3559,13 @@ function useSendMessage() {
3539
3559
  toast.error("There was an error posting your message");
3540
3560
  throw err;
3541
3561
  }
3542
- qc.setQueryData(messagesKeys.list(threadId), [realMessage]);
3562
+ qc.setQueryData(messagesKeys.list(threadId), (old = []) => {
3563
+ const withoutOptimistic = old.filter((m) => !m.optimistic);
3564
+ if (withoutOptimistic.some((m) => m.id === realMessage.id)) {
3565
+ return withoutOptimistic;
3566
+ }
3567
+ return [...withoutOptimistic, realMessage];
3568
+ });
3543
3569
  qc.setQueryData(
3544
3570
  threadsKeys.detail(workspaceId, threadId),
3545
3571
  (old) => old ? { ...old, isFlowRunning: true } : old
@@ -19803,6 +19829,10 @@ function MessageList({
19803
19829
  const messagesEndRef = useRef(null);
19804
19830
  const prevMessageCountRef = useRef(0);
19805
19831
  const hasInitialScrollRef = useRef(false);
19832
+ const everHadMessagesRef = useRef({
19833
+ threadId: "",
19834
+ had: false
19835
+ });
19806
19836
  const isMobile = useIsMobile();
19807
19837
  const { data: activeWorkspace } = useWorkspace(workspaceId);
19808
19838
  const [isAtBottom, setIsAtBottom] = useState(true);
@@ -19874,8 +19904,15 @@ function MessageList({
19874
19904
  ro.observe(content);
19875
19905
  return () => ro.disconnect();
19876
19906
  }, [isAtBottom, scrollToBottom]);
19907
+ const safeMessages = messages ?? [];
19908
+ if (everHadMessagesRef.current.threadId !== threadId) {
19909
+ everHadMessagesRef.current = { threadId, had: safeMessages.length > 0 };
19910
+ } else if (safeMessages.length > 0) {
19911
+ everHadMessagesRef.current.had = true;
19912
+ }
19913
+ const hadMessagesBefore = everHadMessagesRef.current.had;
19877
19914
  const isLoading = isChoosingThread || (threadPending || threadFetching) && !thread || (messagesPending || messagesFetching) && messages === void 0;
19878
- if (isLoading) {
19915
+ if (isLoading && !hadMessagesBefore) {
19879
19916
  return /* @__PURE__ */ jsx(
19880
19917
  "div",
19881
19918
  {
@@ -19891,7 +19928,7 @@ function MessageList({
19891
19928
  }
19892
19929
  );
19893
19930
  }
19894
- if (threadError || messagesError) {
19931
+ if ((threadError || messagesError) && !hadMessagesBefore) {
19895
19932
  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: [
19896
19933
  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: [
19897
19934
  /* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4" }),
@@ -19903,8 +19940,16 @@ function MessageList({
19903
19940
  ] })
19904
19941
  ] }) });
19905
19942
  }
19906
- const safeMessages = messages ?? [];
19907
19943
  if (safeMessages.length === 0) {
19944
+ if (hadMessagesBefore) {
19945
+ return /* @__PURE__ */ jsx(
19946
+ "div",
19947
+ {
19948
+ className: `ss-chat__body flex-shrink-10 flex-1 overflow-y-auto ${hostBg}`,
19949
+ "data-ss-layer": "message-list"
19950
+ }
19951
+ );
19952
+ }
19908
19953
  return /* @__PURE__ */ jsxs("div", { className: "flex overflow-auto flex-shrink-10 flex-col p-8 text-center", children: [
19909
19954
  /* @__PURE__ */ jsx("h3", { className: "text-lg font-medium mb-2", children: activeWorkspace?.name ?? "No messages yet" }),
19910
19955
  activeWorkspace?.firstPrompt && /* @__PURE__ */ jsx("div", { className: "max-w-3xl mx-auto p-4", children: /* @__PURE__ */ jsx(MessageMarkdown, { value: activeWorkspace.firstPrompt }) })