@flamingo-stack/openframe-frontend-core 0.0.288 → 0.0.289-snapshot.20260618174139

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.
Files changed (59) hide show
  1. package/dist/{chunk-6CSW5TMS.cjs → chunk-2V6RCQ5M.cjs} +23 -23
  2. package/dist/{chunk-6CSW5TMS.cjs.map → chunk-2V6RCQ5M.cjs.map} +1 -1
  3. package/dist/{chunk-GJDXIVEQ.js → chunk-4F3X2AOB.js} +2 -2
  4. package/dist/{chunk-TY2EB7VK.js → chunk-4XMYOZFO.js} +2 -2
  5. package/dist/{chunk-RG6FNZUA.cjs → chunk-7L22MF3U.cjs} +3 -3
  6. package/dist/{chunk-RG6FNZUA.cjs.map → chunk-7L22MF3U.cjs.map} +1 -1
  7. package/dist/{chunk-DVUFNTI2.cjs → chunk-AAK6IY6Y.cjs} +4 -4
  8. package/dist/{chunk-DVUFNTI2.cjs.map → chunk-AAK6IY6Y.cjs.map} +1 -1
  9. package/dist/{chunk-AISIZLZP.js → chunk-KJF7SRKH.js} +2 -2
  10. package/dist/{chunk-RWCA2ZQK.js → chunk-LVOBI2M5.js} +2 -2
  11. package/dist/{chunk-6AW25OS6.cjs → chunk-OYXZIPNM.cjs} +25 -25
  12. package/dist/{chunk-6AW25OS6.cjs.map → chunk-OYXZIPNM.cjs.map} +1 -1
  13. package/dist/{chunk-EFYXPR43.js → chunk-R4CLIWAU.js} +33 -5
  14. package/dist/{chunk-EFYXPR43.js.map → chunk-R4CLIWAU.js.map} +1 -1
  15. package/dist/{chunk-JQ4I743L.cjs → chunk-UC5GB255.cjs} +5 -5
  16. package/dist/{chunk-JQ4I743L.cjs.map → chunk-UC5GB255.cjs.map} +1 -1
  17. package/dist/{chunk-OXC72UIP.cjs → chunk-VJ4ZWD5G.cjs} +226 -198
  18. package/dist/chunk-VJ4ZWD5G.cjs.map +1 -0
  19. package/dist/{chunk-ZYLQMCHW.js → chunk-Z5QIVHJW.js} +2 -2
  20. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  21. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
  22. package/dist/components/chat/index.cjs +2 -2
  23. package/dist/components/chat/index.js +1 -1
  24. package/dist/components/chat/types/api.types.d.ts +11 -4
  25. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  26. package/dist/components/chat/utils/history-merge.d.ts +5 -1
  27. package/dist/components/chat/utils/history-merge.d.ts.map +1 -1
  28. package/dist/components/contact/index.cjs +3 -3
  29. package/dist/components/contact/index.js +2 -2
  30. package/dist/components/embeds/index.cjs +3 -3
  31. package/dist/components/embeds/index.js +2 -2
  32. package/dist/components/faq/index.cjs +3 -3
  33. package/dist/components/faq/index.js +2 -2
  34. package/dist/components/features/index.cjs +2 -2
  35. package/dist/components/features/index.js +1 -1
  36. package/dist/components/index.cjs +46 -46
  37. package/dist/components/index.js +5 -5
  38. package/dist/components/navigation/index.cjs +2 -2
  39. package/dist/components/navigation/index.js +1 -1
  40. package/dist/components/related-content/index.cjs +3 -3
  41. package/dist/components/related-content/index.js +2 -2
  42. package/dist/components/tickets/index.cjs +45 -45
  43. package/dist/components/tickets/index.js +3 -3
  44. package/dist/components/ui/index.cjs +2 -2
  45. package/dist/components/ui/index.js +1 -1
  46. package/dist/index.cjs +2 -2
  47. package/dist/index.js +1 -1
  48. package/package.json +1 -1
  49. package/src/components/chat/entity-cards/dispatch.tsx +42 -0
  50. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +3 -1
  51. package/src/components/chat/types/api.types.ts +9 -6
  52. package/src/components/chat/utils/__tests__/history-merge.test.ts +62 -0
  53. package/src/components/chat/utils/history-merge.ts +5 -1
  54. package/dist/chunk-OXC72UIP.cjs.map +0 -1
  55. /package/dist/{chunk-GJDXIVEQ.js.map → chunk-4F3X2AOB.js.map} +0 -0
  56. /package/dist/{chunk-TY2EB7VK.js.map → chunk-4XMYOZFO.js.map} +0 -0
  57. /package/dist/{chunk-AISIZLZP.js.map → chunk-KJF7SRKH.js.map} +0 -0
  58. /package/dist/{chunk-RWCA2ZQK.js.map → chunk-LVOBI2M5.js.map} +0 -0
  59. /package/dist/{chunk-ZYLQMCHW.js.map → chunk-Z5QIVHJW.js.map} +0 -0
@@ -74,6 +74,7 @@ import { MingoIcon } from '../../icons'
74
74
  import { EyeIcon } from '../../icons-v2-generated/interface/eye-icon'
75
75
  import { ArrowRightUpIcon } from '../../icons-v2-generated/arrows/arrow-right-up-icon'
76
76
  import { TagIcon } from '../../icons-v2-generated/shopping/tag-icon'
77
+ import { QuestionCircleIcon } from '../../icons-v2-generated/signs-and-symbols/question-circle-icon'
77
78
  import { SlackLogoGreyIcon } from '../../icons-v2-generated/brand-logos/slack-logo-grey-icon'
78
79
  import { FileContentIcon } from '../../icons-v2-generated/documents/file-content-icon'
79
80
  import { ChartBar01VerIcon } from '../../icons-v2-generated/charts/chart-bar-01-ver-icon'
@@ -354,6 +355,39 @@ function HubspotTicketChatCard({
354
355
  )
355
356
  }
356
357
 
358
+ /** FAQ Q&A — title=question, body=answer preview, optional section pill.
359
+ * No-fetch card: every field the renderer needs already lives on the
360
+ * ChatRef (hub's `FAQ_MAPPER.toRetrievedDoc` populates `title` /
361
+ * `preview` / `url` / `targetPlatform`, and the section name is threaded
362
+ * through `metadata.section` for the badge). The card itself stays pure
363
+ * presentation — same shape as `SlackChatCard` / `HubspotTicketChatCard`. */
364
+ function FaqChatCard({
365
+ chatRef,
366
+ isNewTab,
367
+ discuss,
368
+ }: {
369
+ chatRef: ChatRef
370
+ isNewTab: boolean
371
+ discuss?: CardDiscussAction
372
+ }) {
373
+ const section =
374
+ typeof chatRef.metadata?.section === 'string'
375
+ ? (chatRef.metadata.section as string).trim()
376
+ : undefined
377
+ const statusLabel = section && section.length > 0 ? section : 'FAQ'
378
+ return (
379
+ <MingoInfoCard
380
+ title={chatRef.title}
381
+ description={chatRef.preview ?? undefined}
382
+ icon={<QuestionCircleIcon size={24} />}
383
+ status={{ label: statusLabel, variant: 'grey' }}
384
+ anchorProps={buildAnchorProps(chatRef.url, isNewTab)}
385
+ menuGroups={cardMenuGroups(chatRef.url, discuss)}
386
+ menuAriaLabel="FAQ actions"
387
+ />
388
+ )
389
+ }
390
+
357
391
  function SlackChatCard({
358
392
  chatRef,
359
393
  isNewTab,
@@ -1121,6 +1155,14 @@ const CHAT_CARD_REGISTRY: Record<string, ChatCardRegistryEntry> = {
1121
1155
  discuss={opts.discuss} />
1122
1156
  ),
1123
1157
  },
1158
+ faq: {
1159
+ mode: 'no-fetch',
1160
+ label: 'FAQ',
1161
+ bareInline: true,
1162
+ render: (chatRef, opts) => (
1163
+ <FaqChatCard chatRef={chatRef} isNewTab={opts.isNewTab} discuss={opts.discuss} />
1164
+ ),
1165
+ },
1124
1166
  hubspot_ticket: {
1125
1167
  mode: 'no-fetch',
1126
1168
  label: 'HubSpot ticket',
@@ -345,7 +345,7 @@ export function useRealtimeChunkProcessor(
345
345
  }
346
346
 
347
347
  case 'system': {
348
- callbacks.onSystemMessage?.(action.text)
348
+ callbacks.onSystemMessage?.(action.text, { streamSeq })
349
349
  break
350
350
  }
351
351
 
@@ -354,6 +354,7 @@ export function useRealtimeChunkProcessor(
354
354
  ownerType: action.ownerType,
355
355
  displayName: action.displayName,
356
356
  userId: action.userId,
357
+ streamSeq,
357
358
  })
358
359
  break
359
360
  }
@@ -363,6 +364,7 @@ export function useRealtimeChunkProcessor(
363
364
  ownerType: action.ownerType,
364
365
  displayName: action.displayName,
365
366
  userId: action.userId,
367
+ streamSeq,
366
368
  })
367
369
  break
368
370
 
@@ -164,14 +164,17 @@ export interface RealtimeChunkCallbacks {
164
164
  onSegmentsUpdate?: (segments: MessageSegment[], metadata?: SegmentsUpdateMetadata) => void
165
165
  /** Called when an error is received */
166
166
  onError?: (error: string, details?: string) => void
167
- /** Called when a user message request is received (echo) */
168
- onUserMessage?: (text: string, metadata?: { ownerType?: string; displayName?: string; userId?: string }) => void
167
+ /** Called when a user message request is received (echo). `streamSeq` (when
168
+ * the transport carries one) lets hosts stamp the synthetic so the history
169
+ * merge can dedup it against its persisted twin by sequence. */
170
+ onUserMessage?: (text: string, metadata?: { ownerType?: string; displayName?: string; userId?: string; streamSeq?: number }) => void
169
171
  /** Called when TOKEN_USAGE chunk is received with token stats */
170
172
  onTokenUsage?: (data: TokenUsageData) => void
171
- /** Called when a direct message is received (immediately displayed) */
172
- onDirectMessage?: (text: string, metadata?: { ownerType?: string; displayName?: string; userId?: string }) => void
173
- /** Called when a system message is received (e.g. "User joined the chat") */
174
- onSystemMessage?: (text: string) => void
173
+ /** Called when a direct message is received (immediately displayed). Carries
174
+ * `streamSeq` for the same per-message dedup as `onUserMessage`. */
175
+ onDirectMessage?: (text: string, metadata?: { ownerType?: string; displayName?: string; userId?: string; streamSeq?: number }) => void
176
+ /** Called when a system message is received (e.g. "User joined the chat"). */
177
+ onSystemMessage?: (text: string, metadata?: { streamSeq?: number }) => void
175
178
  /** Callback for approval actions */
176
179
  onApprove?: (requestId?: string) => Promise<void> | void
177
180
  /** Callback for rejection actions */
@@ -363,6 +363,68 @@ describe('mergeHistoryWithRealtime', () => {
363
363
  })
364
364
  expect(ids(merged)).toEqual([U0.id, A0.id, U1.id, A1.id])
365
365
  })
366
+
367
+ // The reported reconnect bug: a replayed direct/system message re-mints a
368
+ // synthetic with a FRESH timestamp (wall-clock can't catch it), so dedup
369
+ // must be by seq. Direct/system are role 'user' rows persisted with a
370
+ // lastChunkStreamSeq equal to the realtime chunk's streamSeq.
371
+ it('drops a replayed direct-message synthetic once history persisted it', () => {
372
+ const histDirect: TestMessage = { id: 'aaaa0009', role: 'user', content: 'ping from tech', timestamp: t(2100) }
373
+ const replayedDirect: TestMessage = {
374
+ id: 'direct-9999-r',
375
+ role: 'user',
376
+ content: 'ping from tech',
377
+ timestamp: t(9999), // re-minted on reconnect replay — newer than the snapshot
378
+ streamSeq: 80,
379
+ }
380
+ const merged = mergeHistoryWithRealtime<TestMessage>({
381
+ processedHistory: [U0, A0, histDirect],
382
+ rawHistoryIds: new Set([U0.id, A0.id, histDirect.id]),
383
+ existingMessages: [U0, A0, histDirect, replayedDirect],
384
+ streamingMessageId: null,
385
+ historyFetchedAt: 5000,
386
+ historyMaxStreamSeq: 80,
387
+ })
388
+ expect(ids(merged)).toEqual([U0.id, A0.id, histDirect.id])
389
+ })
390
+
391
+ it('keeps a live direct message history has not persisted yet (no loss)', () => {
392
+ const liveDirect: TestMessage = {
393
+ id: 'direct-3000-x',
394
+ role: 'user',
395
+ content: 'new tech msg',
396
+ timestamp: t(3000),
397
+ streamSeq: 90,
398
+ }
399
+ const merged = mergeHistoryWithRealtime<TestMessage>({
400
+ processedHistory: [U0, A0],
401
+ existingMessages: [U0, A0, liveDirect],
402
+ streamingMessageId: null,
403
+ historyFetchedAt: 5000,
404
+ historyMaxStreamSeq: 80, // history hasn't reached seq 90 yet
405
+ })
406
+ expect(ids(merged)).toContain(liveDirect.id)
407
+ })
408
+
409
+ it('dedups a system-message synthetic by seq coverage', () => {
410
+ const histSystem: TestMessage = { id: 'aaaa0010', role: 'user', content: 'User joined', timestamp: t(2100) }
411
+ const synSystem: TestMessage = {
412
+ id: 'system-9999-r',
413
+ role: 'user',
414
+ content: 'User joined',
415
+ timestamp: t(9999),
416
+ streamSeq: 70,
417
+ }
418
+ const merged = mergeHistoryWithRealtime<TestMessage>({
419
+ processedHistory: [U0, A0, histSystem],
420
+ rawHistoryIds: new Set([U0.id, A0.id, histSystem.id]),
421
+ existingMessages: [U0, A0, histSystem, synSystem],
422
+ streamingMessageId: null,
423
+ historyFetchedAt: 5000,
424
+ historyMaxStreamSeq: 80,
425
+ })
426
+ expect(ids(merged)).toEqual([U0.id, A0.id, histSystem.id])
427
+ })
366
428
  })
367
429
  })
368
430
 
@@ -36,14 +36,18 @@ export interface MergeableChatMessage {
36
36
 
37
37
  /** Ids minted client-side by realtime chunk processors
38
38
  * (`assistant-<ts>-…` placeholder bubbles, `user-<ts>-…` peer messages,
39
+ * `direct-<ts>-…` technician direct messages, `system-<ts>-…` system notices,
39
40
  * `error-<ts>` stream errors). They never match the Mongo ObjectIds history
40
41
  * returns for the same turns. This is the cross-host contract every minting
41
42
  * site (lib `use-chat`, Mingo / tickets chunk processors, openframe-chat)
42
43
  * must keep matching — exported so it lives in exactly one place.
44
+ * `direct-`/`system-` are persisted (as ADMIN/SYSTEM history rows) and so are
45
+ * replayed by JetStream on reconnect; without them here a replayed direct
46
+ * message renders twice (its persisted twin + the fresh synthetic).
43
47
  * `welcome-` and `optimistic-` ids are intentionally NOT listed: welcome
44
48
  * bubbles are never persisted server-side, and optimistic user messages are
45
49
  * deduped by content below. */
46
- export const SYNTHETIC_REALTIME_ID_PREFIXES = ['assistant-', 'user-', 'error-'] as const
50
+ export const SYNTHETIC_REALTIME_ID_PREFIXES = ['assistant-', 'user-', 'direct-', 'system-', 'error-'] as const
47
51
 
48
52
  function isSyntheticRealtimeId(id: string): boolean {
49
53
  return SYNTHETIC_REALTIME_ID_PREFIXES.some((prefix) => id.startsWith(prefix))