@flamingo-stack/openframe-frontend-core 0.0.537-snapshot.20260814101439 → 0.0.537

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 (137) hide show
  1. package/dist/chat-protocol/events.d.ts +1 -15
  2. package/dist/chat-protocol/events.d.ts.map +1 -1
  3. package/dist/chat-protocol/index.cjs +0 -15
  4. package/dist/chat-protocol/index.cjs.map +1 -1
  5. package/dist/chat-protocol/index.js +0 -15
  6. package/dist/chat-protocol/index.js.map +1 -1
  7. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
  8. package/dist/{chunk-FDKMA7H4.js → chunk-3GXMS4OP.js} +2 -2
  9. package/dist/{chunk-O56ABJ43.js → chunk-5JPSQOZQ.js} +2 -2
  10. package/dist/{chunk-YXKFS6XF.js → chunk-7EHGP4QK.js} +4094 -4250
  11. package/dist/chunk-7EHGP4QK.js.map +1 -0
  12. package/dist/{chunk-MXB347SP.js → chunk-7FXZHI5D.js} +2 -2
  13. package/dist/{chunk-LR7NVXH7.cjs → chunk-7WNJ7RFO.cjs} +14 -14
  14. package/dist/{chunk-LR7NVXH7.cjs.map → chunk-7WNJ7RFO.cjs.map} +1 -1
  15. package/dist/{chunk-5CA7WS2F.js → chunk-C4LEH3VE.js} +2 -2
  16. package/dist/{chunk-KAQG3TDR.cjs → chunk-CATY4DVR.cjs} +87 -87
  17. package/dist/{chunk-KAQG3TDR.cjs.map → chunk-CATY4DVR.cjs.map} +1 -1
  18. package/dist/{chunk-ODENAPJ4.js → chunk-CQQXB25K.js} +2 -2
  19. package/dist/{chunk-APQOHCJH.js → chunk-CZHCXCWL.js} +2 -2
  20. package/dist/{chunk-F3PJTTFA.cjs → chunk-DHSVBGOX.cjs} +9 -9
  21. package/dist/{chunk-F3PJTTFA.cjs.map → chunk-DHSVBGOX.cjs.map} +1 -1
  22. package/dist/{chunk-YMBPNIRC.js → chunk-HRC56BQW.js} +5 -5
  23. package/dist/{chunk-QODV5KPW.cjs → chunk-KKZ2QMQD.cjs} +26 -26
  24. package/dist/{chunk-QODV5KPW.cjs.map → chunk-KKZ2QMQD.cjs.map} +1 -1
  25. package/dist/{chunk-DQAEKYH2.js → chunk-LFMIAPBP.js} +2 -2
  26. package/dist/{chunk-NX3RTMFX.cjs → chunk-MDP3YKFI.cjs} +5 -5
  27. package/dist/{chunk-NX3RTMFX.cjs.map → chunk-MDP3YKFI.cjs.map} +1 -1
  28. package/dist/{chunk-M6Q6BD4I.js → chunk-MOIHWYSA.js} +2 -2
  29. package/dist/{chunk-3NXYQJBY.cjs → chunk-OYTM3YXO.cjs} +7 -7
  30. package/dist/{chunk-3NXYQJBY.cjs.map → chunk-OYTM3YXO.cjs.map} +1 -1
  31. package/dist/{chunk-WPXQ2UBV.cjs → chunk-QPPG3KHW.cjs} +3 -3
  32. package/dist/{chunk-WPXQ2UBV.cjs.map → chunk-QPPG3KHW.cjs.map} +1 -1
  33. package/dist/{chunk-OPFMSQ5Q.js → chunk-SGNWSUL3.js} +6 -6
  34. package/dist/{chunk-OTDF4VUJ.cjs → chunk-SKIM263K.cjs} +37 -37
  35. package/dist/{chunk-OTDF4VUJ.cjs.map → chunk-SKIM263K.cjs.map} +1 -1
  36. package/dist/{chunk-QY7FHRWF.js → chunk-TCSSYTDN.js} +4 -4
  37. package/dist/{chunk-PT34VDFM.cjs → chunk-V4D5MPB7.cjs} +4 -4
  38. package/dist/{chunk-PT34VDFM.cjs.map → chunk-V4D5MPB7.cjs.map} +1 -1
  39. package/dist/{chunk-UV5LYSUJ.cjs → chunk-VSIHO7DJ.cjs} +61 -61
  40. package/dist/{chunk-UV5LYSUJ.cjs.map → chunk-VSIHO7DJ.cjs.map} +1 -1
  41. package/dist/{chunk-SCW6UZ3F.js → chunk-WHUEUACV.js} +2 -2
  42. package/dist/{chunk-64GAFOBF.cjs → chunk-WJIBB3KT.cjs} +31 -31
  43. package/dist/{chunk-64GAFOBF.cjs.map → chunk-WJIBB3KT.cjs.map} +1 -1
  44. package/dist/{chunk-W3MCQKDV.cjs → chunk-YEKXTTAC.cjs} +26 -26
  45. package/dist/{chunk-W3MCQKDV.cjs.map → chunk-YEKXTTAC.cjs.map} +1 -1
  46. package/dist/{chunk-4OHZCP7L.js → chunk-YIDGB5KP.js} +4 -4
  47. package/dist/{chunk-FQ53MYZ4.cjs → chunk-YMPRMZDD.cjs} +37 -193
  48. package/dist/chunk-YMPRMZDD.cjs.map +1 -0
  49. package/dist/{chunk-K5RWDOAC.cjs → chunk-YPI3JIR6.cjs} +11 -11
  50. package/dist/{chunk-K5RWDOAC.cjs.map → chunk-YPI3JIR6.cjs.map} +1 -1
  51. package/dist/components/case-studies/index.cjs +8 -8
  52. package/dist/components/case-studies/index.js +2 -2
  53. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  54. package/dist/components/chat/index.cjs +2 -8
  55. package/dist/components/chat/index.cjs.map +1 -1
  56. package/dist/components/chat/index.d.ts +0 -2
  57. package/dist/components/chat/index.d.ts.map +1 -1
  58. package/dist/components/chat/index.js +1 -7
  59. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
  60. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -1
  61. package/dist/components/chat/ticket-escalated-message.d.ts +8 -2
  62. package/dist/components/chat/ticket-escalated-message.d.ts.map +1 -1
  63. package/dist/components/chat/types/message.types.d.ts +2 -51
  64. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  65. package/dist/components/chat/utils/message-segment-accumulator.d.ts +1 -13
  66. package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
  67. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  68. package/dist/components/contact/index.cjs +3 -3
  69. package/dist/components/contact/index.js +2 -2
  70. package/dist/components/docs/index.cjs +5 -5
  71. package/dist/components/docs/index.js +4 -4
  72. package/dist/components/embeds/index.cjs +3 -3
  73. package/dist/components/embeds/index.js +2 -2
  74. package/dist/components/faq/index.cjs +3 -3
  75. package/dist/components/faq/index.js +2 -2
  76. package/dist/components/features/index.cjs +2 -2
  77. package/dist/components/features/index.js +1 -1
  78. package/dist/components/help-center-pages/index.cjs +22 -22
  79. package/dist/components/help-center-pages/index.js +13 -13
  80. package/dist/components/index.cjs +132 -138
  81. package/dist/components/index.cjs.map +1 -1
  82. package/dist/components/index.js +11 -17
  83. package/dist/components/index.js.map +1 -1
  84. package/dist/components/meeting-scheduler/index.cjs +34 -34
  85. package/dist/components/meeting-scheduler/index.js +3 -3
  86. package/dist/components/navigation/index.cjs +2 -2
  87. package/dist/components/navigation/index.js +1 -1
  88. package/dist/components/onboarding-guides/index.cjs +5 -5
  89. package/dist/components/onboarding-guides/index.js +4 -4
  90. package/dist/components/related-content/index.cjs +3 -3
  91. package/dist/components/related-content/index.js +2 -2
  92. package/dist/components/tickets/index.cjs +6 -6
  93. package/dist/components/tickets/index.js +5 -5
  94. package/dist/components/ui/index.cjs +2 -8
  95. package/dist/components/ui/index.cjs.map +1 -1
  96. package/dist/components/ui/index.js +1 -7
  97. package/dist/index.cjs +2 -8
  98. package/dist/index.cjs.map +1 -1
  99. package/dist/index.js +1 -7
  100. package/dist/utils/index.cjs.map +1 -1
  101. package/dist/utils/index.js.map +1 -1
  102. package/package.json +1 -1
  103. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +0 -44
  104. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +0 -32
  105. package/src/chat-protocol/events.ts +0 -16
  106. package/src/chat-protocol/nats-decoder.ts +0 -27
  107. package/src/components/chat/chat-message-enhanced.tsx +0 -3
  108. package/src/components/chat/index.ts +0 -2
  109. package/src/components/chat/stream/chat-stream-reducer.ts +0 -19
  110. package/src/components/chat/stream/message-mutations.ts +0 -5
  111. package/src/components/chat/ticket-escalated-message.tsx +32 -10
  112. package/src/components/chat/types/message.types.ts +1 -60
  113. package/src/components/chat/utils/message-segment-accumulator.ts +0 -41
  114. package/src/components/chat/utils/process-historical-messages.ts +0 -31
  115. package/dist/chunk-FQ53MYZ4.cjs.map +0 -1
  116. package/dist/chunk-YXKFS6XF.js.map +0 -1
  117. package/dist/components/chat/ai-assistant-info.d.ts +0 -31
  118. package/dist/components/chat/ai-assistant-info.d.ts.map +0 -1
  119. package/dist/components/chat/ticket-event-message.d.ts +0 -24
  120. package/dist/components/chat/ticket-event-message.d.ts.map +0 -1
  121. package/src/components/chat/__tests__/ticket-event.test.ts +0 -194
  122. package/src/components/chat/ai-assistant-info.tsx +0 -71
  123. package/src/components/chat/ticket-event-message.tsx +0 -79
  124. package/src/stories/TicketEventMessage.stories.tsx +0 -100
  125. /package/dist/{chunk-FDKMA7H4.js.map → chunk-3GXMS4OP.js.map} +0 -0
  126. /package/dist/{chunk-O56ABJ43.js.map → chunk-5JPSQOZQ.js.map} +0 -0
  127. /package/dist/{chunk-MXB347SP.js.map → chunk-7FXZHI5D.js.map} +0 -0
  128. /package/dist/{chunk-5CA7WS2F.js.map → chunk-C4LEH3VE.js.map} +0 -0
  129. /package/dist/{chunk-ODENAPJ4.js.map → chunk-CQQXB25K.js.map} +0 -0
  130. /package/dist/{chunk-APQOHCJH.js.map → chunk-CZHCXCWL.js.map} +0 -0
  131. /package/dist/{chunk-YMBPNIRC.js.map → chunk-HRC56BQW.js.map} +0 -0
  132. /package/dist/{chunk-DQAEKYH2.js.map → chunk-LFMIAPBP.js.map} +0 -0
  133. /package/dist/{chunk-M6Q6BD4I.js.map → chunk-MOIHWYSA.js.map} +0 -0
  134. /package/dist/{chunk-OPFMSQ5Q.js.map → chunk-SGNWSUL3.js.map} +0 -0
  135. /package/dist/{chunk-QY7FHRWF.js.map → chunk-TCSSYTDN.js.map} +0 -0
  136. /package/dist/{chunk-SCW6UZ3F.js.map → chunk-WHUEUACV.js.map} +0 -0
  137. /package/dist/{chunk-4OHZCP7L.js.map → chunk-YIDGB5KP.js.map} +0 -0
@@ -1,31 +0,0 @@
1
- import { type HTMLAttributes, type ReactNode } from "react";
2
- export interface AiAssistantInfoProps extends HTMLAttributes<HTMLDivElement> {
3
- /** Rendered inside the standard 48px framed box (typically a 24px icon). */
4
- icon?: ReactNode;
5
- /** Replaces the framed icon box entirely (e.g. a round 48px avatar for the
6
- * `direct-chat` variant). Wins over `icon` when both are set. */
7
- leading?: ReactNode;
8
- title: string;
9
- /** Secondary line under the title. Omitted → the row is title-only. */
10
- body?: ReactNode;
11
- /** Timestamp of the message row this block came in on. */
12
- timestamp?: Date;
13
- }
14
- /**
15
- * Figma `ai-assistant-info` — the shared in-thread status card: a framed
16
- * icon (or avatar), a title with a right-aligned time, and a secondary line.
17
- * One presentational shell for every lifecycle receipt in a chat thread
18
- * (handoff, technician joined, ticket resolved/reopened, …); the per-event
19
- * components (`TicketEscalatedMessage`, `TicketEventMessage`) own the copy
20
- * and icon choice and delegate the chrome here.
21
- *
22
- * The body WRAPS, deliberately diverging from the mock's single-line
23
- * ellipsis: the mock's 72px comes from its placeholder copy fitting one
24
- * line, while real wire text is a full sentence, and clipping the only
25
- * explanation of a lifecycle change is not recoverable on touch. The title
26
- * still truncates — it holds one line so the timestamp is never pushed out
27
- * of the row at narrow widths.
28
- */
29
- declare const AiAssistantInfo: import("react").ForwardRefExoticComponent<AiAssistantInfoProps & import("react").RefAttributes<HTMLDivElement>>;
30
- export { AiAssistantInfo };
31
- //# sourceMappingURL=ai-assistant-info.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ai-assistant-info.d.ts","sourceRoot":"","sources":["../../../src/components/chat/ai-assistant-info.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,cAAc,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAIvE,MAAM,WAAW,oBAAqB,SAAQ,cAAc,CAAC,cAAc,CAAC;IAC1E,4EAA4E;IAC5E,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;sEACkE;IAClE,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,uEAAuE;IACvE,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,IAAI,CAAA;CACjB;AAED;;;;;;;;;;;;;;GAcG;AACH,QAAA,MAAM,eAAe,iHAgCpB,CAAA;AAID,OAAO,EAAE,eAAe,EAAE,CAAA"}
@@ -1,24 +0,0 @@
1
- import { type HTMLAttributes } from "react";
2
- import { type TicketEventSegment } from "./types/message.types";
3
- export interface TicketEventMessageProps extends HTMLAttributes<HTMLDivElement> {
4
- data: TicketEventSegment['data'];
5
- /** Timestamp of the message row this block came in on. */
6
- timestamp?: Date;
7
- }
8
- /**
9
- * Ticket lifecycle receipt — resolved / reopened / an unknown future kind.
10
- * Figma `ai-assistant-info` (type=resolved-fae | resolved-tech |
11
- * reopened-fae | reopened-tech | reopened-reason).
12
- *
13
- * Copy is composed client-side from the event's fields (unlike
14
- * `ticket_escalated`, whose body the backend authors):
15
- * RESOLVED → "Resolved by {actorName}." under a green check;
16
- * REOPENED → the reason when the wire carries one, otherwise who picks the
17
- * conversation up next, inferred from `actorType` (an AI actor keeps
18
- * assisting; anything else means a technician will reply);
19
- * unknown kind → a neutral info line: humanized kind as the title, the
20
- * reason or actor as the body. Never dropped — the vocabulary is open.
21
- */
22
- declare const TicketEventMessage: import("react").ForwardRefExoticComponent<TicketEventMessageProps & import("react").RefAttributes<HTMLDivElement>>;
23
- export { TicketEventMessage };
24
- //# sourceMappingURL=ticket-event-message.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ticket-event-message.d.ts","sourceRoot":"","sources":["../../../src/components/chat/ticket-event-message.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,OAAO,CAAA;AAGvD,OAAO,EAAqB,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAoBlF,MAAM,WAAW,uBAAwB,SAAQ,cAAc,CAAC,cAAc,CAAC;IAC7E,IAAI,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAA;IAChC,0DAA0D;IAC1D,SAAS,CAAC,EAAE,IAAI,CAAA;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,QAAA,MAAM,kBAAkB,oHA6BvB,CAAA;AAID,OAAO,EAAE,kBAAkB,EAAE,CAAA"}
@@ -1,194 +0,0 @@
1
- /**
2
- * TICKET_EVENT — the ticket lifecycle receipt (resolved / reopened / open
3
- * vocabulary), across both the live NATS path and history replay.
4
- *
5
- * The behaviours worth pinning:
6
- * - the chunk arrives STANDALONE (no MESSAGE_START at all — resolution can
7
- * fire from a PSA-side action with no turn open), so it must mint/append
8
- * to the trailing bubble like `ticket-escalated` does;
9
- * - dedupe identity is the stream sequence, with payload-equality as the
10
- * fallback for the hydrate-then-catch-up overlap where the hydrated twin
11
- * is seq-less (the persisted row's seq lives on the message, not in
12
- * `messageData`);
13
- * - `kind` is OPEN: an unknown kind decodes and renders, only a missing
14
- * kind is malformed.
15
- */
16
-
17
- import { describe, expect, it } from 'vitest'
18
- import { decodeNatsChunk } from '../../../chat-protocol/nats-decoder'
19
- import { createChatStreamReducer } from '../stream/chat-stream-reducer'
20
- import { MessageSegmentAccumulator } from '../utils/message-segment-accumulator'
21
- import { processHistoricalMessages } from '../utils/process-historical-messages'
22
- import type { HistoricalMessage, MessageSegment, TicketEventSegment } from '../types'
23
-
24
- const resolvedChunk = (streamSeq: number, over: Record<string, unknown> = {}) => ({
25
- type: 'TICKET_EVENT',
26
- kind: 'RESOLVED',
27
- actorId: 'fae',
28
- actorName: 'Fae',
29
- actorType: 'AI',
30
- streamSeq,
31
- ...over,
32
- })
33
-
34
- const events = (segments: MessageSegment[] | string | undefined): TicketEventSegment[] =>
35
- Array.isArray(segments)
36
- ? (segments.filter((s) => s.type === 'ticket_event') as TicketEventSegment[])
37
- : []
38
-
39
- describe('decodeNatsChunk — TICKET_EVENT', () => {
40
- it('decodes the resolution receipt with its actor fields', () => {
41
- expect(decodeNatsChunk(resolvedChunk(7))).toEqual({
42
- type: 'ticket-event',
43
- kind: 'RESOLVED',
44
- actorId: 'fae',
45
- actorName: 'Fae',
46
- actorType: 'AI',
47
- reason: undefined,
48
- seq: 7,
49
- })
50
- })
51
-
52
- it('falls back to the payload `sequenceId` when the transport did not stamp `streamSeq`', () => {
53
- const event = decodeNatsChunk({ type: 'TICKET_EVENT', kind: 'REOPENED', sequenceId: 42 })
54
- expect(event).toMatchObject({ type: 'ticket-event', seq: 42 })
55
- })
56
-
57
- it('prefers the transport-stamped `streamSeq` over the payload copy', () => {
58
- expect(decodeNatsChunk(resolvedChunk(500, { sequenceId: 499 }))).toMatchObject({ seq: 500 })
59
- })
60
-
61
- it('keeps unknown kinds — the vocabulary is open', () => {
62
- expect(decodeNatsChunk({ type: 'TICKET_EVENT', kind: 'ON_HOLD', streamSeq: 1 })).toMatchObject({
63
- type: 'ticket-event',
64
- kind: 'ON_HOLD',
65
- })
66
- })
67
-
68
- it('drops only a chunk with no kind at all', () => {
69
- expect(decodeNatsChunk({ type: 'TICKET_EVENT', streamSeq: 1 })).toBeNull()
70
- expect(decodeNatsChunk({ type: 'TICKET_EVENT', kind: ' ', streamSeq: 1 })).toBeNull()
71
- })
72
- })
73
-
74
- describe('chat stream reducer — ticket event', () => {
75
- const trailingSegments = (reducer: ReturnType<typeof createChatStreamReducer>): MessageSegment[] => {
76
- const { messages } = reducer.state
77
- return messages[messages.length - 1]?.segments ?? []
78
- }
79
-
80
- const feed = (reducer: ReturnType<typeof createChatStreamReducer>, chunks: unknown[]) => {
81
- for (const chunk of chunks) {
82
- const event = decodeNatsChunk(chunk)
83
- if (event) reducer.apply(event)
84
- }
85
- }
86
-
87
- it('renders a receipt arriving with no turn open at all', () => {
88
- const reducer = createChatStreamReducer({ transport: 'nats' })
89
- feed(reducer, [resolvedChunk(1)])
90
- expect(events(trailingSegments(reducer))).toHaveLength(1)
91
- })
92
-
93
- it('appends to the trailing bubble without wiping the reply the user is reading', () => {
94
- const reducer = createChatStreamReducer({ transport: 'nats' })
95
- feed(reducer, [
96
- { type: 'MESSAGE_START', streamSeq: 1 },
97
- { type: 'TEXT', text: 'Excellent! Feel free to reach out.', streamSeq: 2 },
98
- { type: 'MESSAGE_END', streamSeq: 3 },
99
- resolvedChunk(4),
100
- ])
101
- const segments = trailingSegments(reducer)
102
- expect(segments.find((s: MessageSegment) => s.type === 'text')).toMatchObject({
103
- text: 'Excellent! Feel free to reach out.',
104
- })
105
- expect(events(segments)).toHaveLength(1)
106
- })
107
-
108
- it('is idempotent across a redelivered chunk (JetStream catch-up)', () => {
109
- const reducer = createChatStreamReducer({ transport: 'nats' })
110
- feed(reducer, [resolvedChunk(2), { type: 'MESSAGE_END', streamSeq: 3 }])
111
- // A replay re-decodes the same chunk; the seq gate drops it, but the
112
- // upsert must hold even if the gate ever moves.
113
- const event = decodeNatsChunk(resolvedChunk(2))
114
- if (event) reducer.apply(event)
115
- expect(events(trailingSegments(reducer))).toHaveLength(1)
116
- })
117
-
118
- it('keeps distinct lifecycle events distinct (resolve → reopen → resolve)', () => {
119
- const reducer = createChatStreamReducer({ transport: 'nats' })
120
- feed(reducer, [
121
- resolvedChunk(1),
122
- { type: 'TICKET_EVENT', kind: 'REOPENED', reason: 'Still broken', streamSeq: 2 },
123
- resolvedChunk(3, { actorName: 'Roman Smith', actorType: 'TECHNICIAN', actorId: 'u-1' }),
124
- ])
125
- expect(events(trailingSegments(reducer))).toHaveLength(3)
126
- })
127
- })
128
-
129
- describe('accumulator — hydrate/catch-up overlap', () => {
130
- it('matches a seq-less hydrated twin by payload equality instead of stacking', () => {
131
- const accumulator = new MessageSegmentAccumulator()
132
- // Hydrated from history: the persisted row's seq lives on the message,
133
- // so the restored segment carries none.
134
- accumulator.addTicketEvent({ kind: 'RESOLVED', actorId: 'fae', actorName: 'Fae', actorType: 'AI' })
135
- // The same event redelivered live, now with its stream sequence.
136
- const segments = accumulator.addTicketEvent(
137
- { kind: 'RESOLVED', actorId: 'fae', actorName: 'Fae', actorType: 'AI' },
138
- 412,
139
- )
140
- const rendered = events(segments)
141
- expect(rendered).toHaveLength(1)
142
- expect(rendered[0].streamSeq).toBe(412)
143
- })
144
-
145
- it('does NOT collapse two different events of the same kind', () => {
146
- const accumulator = new MessageSegmentAccumulator()
147
- accumulator.addTicketEvent({ kind: 'RESOLVED', actorName: 'Fae', actorType: 'AI' }, 1)
148
- const segments = accumulator.addTicketEvent(
149
- { kind: 'RESOLVED', actorName: 'Roman Smith', actorType: 'TECHNICIAN' },
150
- 9,
151
- )
152
- expect(events(segments)).toHaveLength(2)
153
- })
154
- })
155
-
156
- describe('processHistoricalMessages — ticket event', () => {
157
- const historyRow = (id: string, data: Record<string, unknown>): HistoricalMessage => ({
158
- id,
159
- createdAt: '2026-08-06T12:00:00Z',
160
- owner: { type: 'ASSISTANT' },
161
- messageData: [data as never],
162
- })
163
-
164
- it('replays the persisted row through the same mapper as the live path', () => {
165
- const { messages } = processHistoricalMessages([
166
- historyRow('m1', {
167
- type: 'TICKET_EVENT',
168
- kind: 'REOPENED',
169
- actorId: 'user-42',
170
- actorName: 'John Smith',
171
- actorType: 'CLIENT',
172
- reason: 'The printer stopped working again',
173
- }),
174
- ])
175
- expect(messages).toHaveLength(1)
176
- expect(events(messages[0].content)[0]).toMatchObject({
177
- data: { kind: 'REOPENED', reason: 'The printer stopped working again' },
178
- })
179
- })
180
-
181
- it('keeps an unknown persisted kind renderable', () => {
182
- const { messages } = processHistoricalMessages([
183
- historyRow('m1', { type: 'TICKET_EVENT', kind: 'ON_HOLD' }),
184
- ])
185
- expect(events(messages[0]?.content ?? [])).toHaveLength(1)
186
- })
187
-
188
- it('skips a malformed row with no kind', () => {
189
- const { messages } = processHistoricalMessages([
190
- historyRow('m1', { type: 'TICKET_EVENT', actorName: 'Fae' }),
191
- ])
192
- expect(messages.flatMap((m) => events(m.content))).toHaveLength(0)
193
- })
194
- })
@@ -1,71 +0,0 @@
1
- "use client"
2
-
3
- import { forwardRef, type HTMLAttributes, type ReactNode } from "react"
4
- import { cn } from "../../utils/cn"
5
- import { formatTime } from "../../utils/format-date"
6
-
7
- export interface AiAssistantInfoProps extends HTMLAttributes<HTMLDivElement> {
8
- /** Rendered inside the standard 48px framed box (typically a 24px icon). */
9
- icon?: ReactNode
10
- /** Replaces the framed icon box entirely (e.g. a round 48px avatar for the
11
- * `direct-chat` variant). Wins over `icon` when both are set. */
12
- leading?: ReactNode
13
- title: string
14
- /** Secondary line under the title. Omitted → the row is title-only. */
15
- body?: ReactNode
16
- /** Timestamp of the message row this block came in on. */
17
- timestamp?: Date
18
- }
19
-
20
- /**
21
- * Figma `ai-assistant-info` — the shared in-thread status card: a framed
22
- * icon (or avatar), a title with a right-aligned time, and a secondary line.
23
- * One presentational shell for every lifecycle receipt in a chat thread
24
- * (handoff, technician joined, ticket resolved/reopened, …); the per-event
25
- * components (`TicketEscalatedMessage`, `TicketEventMessage`) own the copy
26
- * and icon choice and delegate the chrome here.
27
- *
28
- * The body WRAPS, deliberately diverging from the mock's single-line
29
- * ellipsis: the mock's 72px comes from its placeholder copy fitting one
30
- * line, while real wire text is a full sentence, and clipping the only
31
- * explanation of a lifecycle change is not recoverable on touch. The title
32
- * still truncates — it holds one line so the timestamp is never pushed out
33
- * of the row at narrow widths.
34
- */
35
- const AiAssistantInfo = forwardRef<HTMLDivElement, AiAssistantInfoProps>(
36
- ({ className, icon, leading, title, body, timestamp, ...props }, ref) => {
37
- return (
38
- <div
39
- ref={ref}
40
- className={cn(
41
- "mb-[var(--spacing-system-xsf)] flex items-start gap-[var(--spacing-system-s)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-s)]",
42
- className,
43
- )}
44
- {...props}
45
- >
46
- {leading ?? (
47
- <div className="flex size-12 shrink-0 items-center justify-center rounded-md border border-ods-border bg-ods-bg">
48
- {icon}
49
- </div>
50
- )}
51
- <div className="flex min-w-0 flex-1 flex-col">
52
- <div className="flex w-full items-start">
53
- <p className="text-h4 min-w-0 flex-1 truncate text-ods-text-primary" title={title}>
54
- {title}
55
- </p>
56
- {timestamp && (
57
- <span className="text-h6 shrink-0 text-right text-ods-text-secondary">{formatTime(timestamp)}</span>
58
- )}
59
- </div>
60
- {body != null && body !== "" && (
61
- <p className="text-h4 w-full break-words text-ods-text-secondary">{body}</p>
62
- )}
63
- </div>
64
- </div>
65
- )
66
- },
67
- )
68
-
69
- AiAssistantInfo.displayName = "AiAssistantInfo"
70
-
71
- export { AiAssistantInfo }
@@ -1,79 +0,0 @@
1
- "use client"
2
-
3
- import { forwardRef, type HTMLAttributes } from "react"
4
- import { CheckCircleIcon, InfoCircleIcon } from "../icons-v2-generated"
5
- import { AiAssistantInfo } from "./ai-assistant-info"
6
- import { TICKET_EVENT_KIND, type TicketEventSegment } from "./types/message.types"
7
-
8
- /** Actor types the backend uses for the AI agent. Open set on the wire —
9
- * compared case-insensitively; anything else is treated as a human. */
10
- const AI_ACTOR_TYPES = new Set(["AI", "AGENT", "ASSISTANT", "BOT"])
11
-
12
- function isAiActor(actorType?: string): boolean {
13
- return !!actorType && AI_ACTOR_TYPES.has(actorType.toUpperCase())
14
- }
15
-
16
- /** `TICKET_ON_HOLD` → `Ticket On Hold` — neutral title for kinds this build
17
- * does not know, which must still render (the vocabulary is open). */
18
- function humanizeKind(kind: string): string {
19
- return kind
20
- .split(/[_\s-]+/)
21
- .filter(Boolean)
22
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
23
- .join(" ")
24
- }
25
-
26
- export interface TicketEventMessageProps extends HTMLAttributes<HTMLDivElement> {
27
- data: TicketEventSegment['data']
28
- /** Timestamp of the message row this block came in on. */
29
- timestamp?: Date
30
- }
31
-
32
- /**
33
- * Ticket lifecycle receipt — resolved / reopened / an unknown future kind.
34
- * Figma `ai-assistant-info` (type=resolved-fae | resolved-tech |
35
- * reopened-fae | reopened-tech | reopened-reason).
36
- *
37
- * Copy is composed client-side from the event's fields (unlike
38
- * `ticket_escalated`, whose body the backend authors):
39
- * RESOLVED → "Resolved by {actorName}." under a green check;
40
- * REOPENED → the reason when the wire carries one, otherwise who picks the
41
- * conversation up next, inferred from `actorType` (an AI actor keeps
42
- * assisting; anything else means a technician will reply);
43
- * unknown kind → a neutral info line: humanized kind as the title, the
44
- * reason or actor as the body. Never dropped — the vocabulary is open.
45
- */
46
- const TicketEventMessage = forwardRef<HTMLDivElement, TicketEventMessageProps>(
47
- ({ data, timestamp, ...props }, ref) => {
48
- const { kind, actorName, actorType, reason } = data
49
- const resolved = kind === TICKET_EVENT_KIND.RESOLVED
50
- const reopened = kind === TICKET_EVENT_KIND.REOPENED
51
-
52
- const title = resolved ? "Ticket Resolved" : reopened ? "Ticket Reopened" : humanizeKind(kind)
53
-
54
- let body: string | undefined
55
- if (resolved) {
56
- body = actorName ? `Resolved by ${actorName}.` : "The ticket has been resolved."
57
- } else if (reopened) {
58
- body = reason
59
- ? `Reason: ${reason}`
60
- : isAiActor(actorType)
61
- ? `${actorName || "Fae"} will continue assisting in this conversation.`
62
- : "A technician will reply when available."
63
- } else {
64
- body = reason || (actorName ? `By ${actorName}.` : undefined)
65
- }
66
-
67
- const icon = resolved ? (
68
- <CheckCircleIcon className="size-6 text-ods-attention-green-success" />
69
- ) : (
70
- <InfoCircleIcon className="size-6 text-ods-flamingo-pink" />
71
- )
72
-
73
- return <AiAssistantInfo ref={ref} icon={icon} title={title} body={body} timestamp={timestamp} {...props} />
74
- },
75
- )
76
-
77
- TicketEventMessage.displayName = "TicketEventMessage"
78
-
79
- export { TicketEventMessage }
@@ -1,100 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
- import type React from "react";
3
-
4
- import { TicketEventMessage } from "../components/chat/ticket-event-message";
5
-
6
- /**
7
- * `ticket_event` message segment — the ticket lifecycle receipt (Figma:
8
- * `ai-assistant-info`, types resolved-fae / resolved-tech / reopened-fae /
9
- * reopened-tech / reopened-reason).
10
- *
11
- * Decoded from the standalone `TICKET_EVENT` chunk (live) or the persisted
12
- * `TicketEventData` row (history). `kind` is an OPEN vocabulary: unknown
13
- * kinds render as a neutral info line rather than being dropped, so the
14
- * backend can add lifecycle kinds without a client release.
15
- */
16
-
17
- /** Fixed so the timestamp doesn't churn between snapshots. */
18
- const TIMESTAMP = new Date("2026-08-07T14:47:00");
19
-
20
- const plainDecorator = (Story: React.ComponentType) => (
21
- <div style={{ maxWidth: 600, background: "var(--color-bg)" }} className="p-4">
22
- <Story />
23
- </div>
24
- );
25
-
26
- const meta = {
27
- title: "Chat/Client/TicketEventMessage",
28
- component: TicketEventMessage,
29
- tags: ["autodocs"],
30
- parameters: {
31
- docs: {
32
- description: {
33
- component:
34
- "Renders a `TICKET_EVENT` block: a green check for RESOLVED, a pink info tile for REOPENED and for any kind this build doesn't know, with copy composed from the event's actor and reason fields.",
35
- },
36
- },
37
- },
38
- decorators: [plainDecorator],
39
- } satisfies Meta<typeof TicketEventMessage>;
40
-
41
- export default meta;
42
- type Story = StoryObj<typeof meta>;
43
-
44
- /** Fae (the AI agent) resolved the ticket — Figma type=resolved-fae. */
45
- export const ResolvedByFae: Story = {
46
- args: {
47
- data: { kind: "RESOLVED", actorId: "fae", actorName: "Fae", actorType: "AI" },
48
- timestamp: TIMESTAMP,
49
- },
50
- };
51
-
52
- /** A human technician resolved the ticket — Figma type=resolved-tech. */
53
- export const ResolvedByTechnician: Story = {
54
- args: {
55
- data: { kind: "RESOLVED", actorId: "u-42", actorName: "Roman Smith", actorType: "TECHNICIAN" },
56
- timestamp: TIMESTAMP,
57
- },
58
- };
59
-
60
- /** Reopened with the user's stated reason — Figma type=reopened-reason. */
61
- export const ReopenedWithReason: Story = {
62
- args: {
63
- data: {
64
- kind: "REOPENED",
65
- actorId: "u-7",
66
- actorName: "John Smith",
67
- actorType: "CLIENT",
68
- reason: "The printer stopped working again",
69
- },
70
- timestamp: TIMESTAMP,
71
- },
72
- };
73
-
74
- /** Reopened, no reason, AI keeps assisting — Figma type=reopened-fae. */
75
- export const ReopenedFaeContinues: Story = {
76
- args: {
77
- data: { kind: "REOPENED", actorId: "fae", actorName: "Fae", actorType: "AI" },
78
- timestamp: TIMESTAMP,
79
- },
80
- };
81
-
82
- /** Reopened, no reason, routed to a human — Figma type=reopened-tech. */
83
- export const ReopenedTechnicianReplies: Story = {
84
- args: {
85
- data: { kind: "REOPENED", actorId: "u-42", actorName: "Roman Smith", actorType: "TECHNICIAN" },
86
- timestamp: TIMESTAMP,
87
- },
88
- };
89
-
90
- /**
91
- * A kind the client has never seen. The open vocabulary means the backend can
92
- * add lifecycle kinds without a client release — this story is the regression
93
- * guard: neutral info line, humanized title, never dropped.
94
- */
95
- export const UnknownKind: Story = {
96
- args: {
97
- data: { kind: "ON_HOLD", actorName: "Roman Smith", reason: "Waiting for replacement hardware" },
98
- timestamp: TIMESTAMP,
99
- },
100
- };