@flamingo-stack/openframe-frontend-core 0.0.587-1973.5528.1 → 0.0.587-2000.5532.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.
Files changed (165) hide show
  1. package/dist/chat-protocol/events.d.ts +2 -8
  2. package/dist/chat-protocol/events.d.ts.map +1 -1
  3. package/dist/chat-protocol/index.cjs +0 -188
  4. package/dist/chat-protocol/index.cjs.map +1 -1
  5. package/dist/chat-protocol/index.js +0 -188
  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-5VCDP6UA.cjs → chunk-3FHWLQDG.cjs} +10 -10
  9. package/dist/{chunk-5VCDP6UA.cjs.map → chunk-3FHWLQDG.cjs.map} +1 -1
  10. package/dist/{chunk-RE3KVI2Q.js → chunk-3PDTGDOA.js} +5 -5
  11. package/dist/{chunk-5X7AJ67N.js → chunk-436G4DI4.js} +3 -3
  12. package/dist/{chunk-BLC4JOFC.cjs → chunk-4IIFSFJ3.cjs} +15 -15
  13. package/dist/{chunk-BLC4JOFC.cjs.map → chunk-4IIFSFJ3.cjs.map} +1 -1
  14. package/dist/{chunk-JR37UTA2.cjs → chunk-5O5JMGEN.cjs} +33 -33
  15. package/dist/{chunk-JR37UTA2.cjs.map → chunk-5O5JMGEN.cjs.map} +1 -1
  16. package/dist/{chunk-7A6J4JTW.js → chunk-6BFEJXMI.js} +3 -3
  17. package/dist/{chunk-S2UY2C4A.cjs → chunk-7WIKOL25.cjs} +10 -10
  18. package/dist/{chunk-S2UY2C4A.cjs.map → chunk-7WIKOL25.cjs.map} +1 -1
  19. package/dist/{chunk-DVMQV3M4.cjs → chunk-A5PXHMCJ.cjs} +69 -69
  20. package/dist/{chunk-DVMQV3M4.cjs.map → chunk-A5PXHMCJ.cjs.map} +1 -1
  21. package/dist/{chunk-RQ5PYSRJ.js → chunk-BLJEPNMJ.js} +4 -4
  22. package/dist/{chunk-4MVGFYRE.js → chunk-D7YISHXC.js} +190 -534
  23. package/dist/chunk-D7YISHXC.js.map +1 -0
  24. package/dist/{chunk-4S7GMJYA.cjs → chunk-DMQMRFGS.cjs} +21 -21
  25. package/dist/{chunk-4S7GMJYA.cjs.map → chunk-DMQMRFGS.cjs.map} +1 -1
  26. package/dist/{chunk-AIUIIGO3.cjs → chunk-DTQCZ6AR.cjs} +24 -24
  27. package/dist/{chunk-AIUIIGO3.cjs.map → chunk-DTQCZ6AR.cjs.map} +1 -1
  28. package/dist/{chunk-QKAUBYU5.cjs → chunk-E7NFHRLX.cjs} +3 -3
  29. package/dist/{chunk-QKAUBYU5.cjs.map → chunk-E7NFHRLX.cjs.map} +1 -1
  30. package/dist/{chunk-WFWWQO2I.js → chunk-F3YRPKUA.js} +2 -2
  31. package/dist/{chunk-EKKR5ZFB.cjs → chunk-G45RGWHF.cjs} +543 -887
  32. package/dist/chunk-G45RGWHF.cjs.map +1 -0
  33. package/dist/{chunk-ZRFSUCAH.js → chunk-HCWZP44O.js} +5 -3
  34. package/dist/{chunk-ZRFSUCAH.js.map → chunk-HCWZP44O.js.map} +1 -1
  35. package/dist/{chunk-NEI7XMCM.cjs → chunk-IGBNEPTD.cjs} +9 -9
  36. package/dist/{chunk-NEI7XMCM.cjs.map → chunk-IGBNEPTD.cjs.map} +1 -1
  37. package/dist/{chunk-3UJMZADA.cjs → chunk-KQ5JQVYL.cjs} +43 -43
  38. package/dist/{chunk-3UJMZADA.cjs.map → chunk-KQ5JQVYL.cjs.map} +1 -1
  39. package/dist/{chunk-FDWGO244.js → chunk-LODCKZ72.js} +2 -2
  40. package/dist/{chunk-ULOMJH7C.cjs → chunk-MF7TMFVB.cjs} +5 -5
  41. package/dist/{chunk-ULOMJH7C.cjs.map → chunk-MF7TMFVB.cjs.map} +1 -1
  42. package/dist/{chunk-DSXD77KX.cjs → chunk-MXCWOMNT.cjs} +3 -3
  43. package/dist/{chunk-DSXD77KX.cjs.map → chunk-MXCWOMNT.cjs.map} +1 -1
  44. package/dist/{chunk-E2QFZDCP.js → chunk-OKD7MX7Y.js} +5 -5
  45. package/dist/{chunk-OW3FUJ2I.js → chunk-P24OI2CD.js} +3 -3
  46. package/dist/{chunk-QSO24MEK.js → chunk-R2U2D7LU.js} +2 -2
  47. package/dist/{chunk-KQWI25NP.js → chunk-SI57FQZ2.js} +2 -2
  48. package/dist/{chunk-JQO47UHC.js → chunk-SVR75PFW.js} +5 -5
  49. package/dist/{chunk-UYHETUX3.cjs → chunk-UBSIENYF.cjs} +162 -160
  50. package/dist/chunk-UBSIENYF.cjs.map +1 -0
  51. package/dist/{chunk-OZRT3ZI3.js → chunk-V4WUSOGS.js} +3 -3
  52. package/dist/{chunk-UODM3HJC.js → chunk-VWNU2PFX.js} +3 -3
  53. package/dist/{chunk-SCVIBVFS.cjs → chunk-WQXWH7LF.cjs} +4 -4
  54. package/dist/{chunk-SCVIBVFS.cjs.map → chunk-WQXWH7LF.cjs.map} +1 -1
  55. package/dist/{chunk-7COFWOCJ.cjs → chunk-XNEPABUD.cjs} +22 -22
  56. package/dist/{chunk-7COFWOCJ.cjs.map → chunk-XNEPABUD.cjs.map} +1 -1
  57. package/dist/{chunk-QYDIVL4X.js → chunk-YB5DFQKD.js} +6 -6
  58. package/dist/components/case-studies/index.cjs +9 -9
  59. package/dist/components/case-studies/index.js +3 -3
  60. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  61. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  62. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  63. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  64. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +1 -4
  65. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  66. package/dist/components/chat/hooks/use-slash-commands.d.ts +3 -3
  67. package/dist/components/chat/hooks/use-slash-commands.d.ts.map +1 -1
  68. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +28 -2
  69. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  70. package/dist/components/chat/index.cjs +2 -2
  71. package/dist/components/chat/index.js +1 -1
  72. package/dist/components/chat/remark-strip-citations.d.ts.map +1 -1
  73. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
  74. package/dist/components/chat/tool-execution-display.d.ts.map +1 -1
  75. package/dist/components/chat/types/component.types.d.ts +0 -4
  76. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  77. package/dist/components/chat/types/message.types.d.ts +1 -35
  78. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  79. package/dist/components/chat/types/unified-chat-state.types.d.ts +3 -3
  80. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  81. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  82. package/dist/components/contact/index.cjs +4 -4
  83. package/dist/components/contact/index.js +3 -3
  84. package/dist/components/docs/index.cjs +7 -7
  85. package/dist/components/docs/index.js +6 -6
  86. package/dist/components/embeds/index.cjs +4 -4
  87. package/dist/components/embeds/index.js +3 -3
  88. package/dist/components/faq/index.cjs +4 -4
  89. package/dist/components/faq/index.js +3 -3
  90. package/dist/components/features/index.cjs +3 -3
  91. package/dist/components/features/index.js +2 -2
  92. package/dist/components/help-center-pages/index.cjs +23 -23
  93. package/dist/components/help-center-pages/index.js +14 -14
  94. package/dist/components/index.cjs +36 -36
  95. package/dist/components/index.js +13 -13
  96. package/dist/components/meeting-scheduler/index.cjs +21 -21
  97. package/dist/components/meeting-scheduler/index.js +2 -2
  98. package/dist/components/navigation/index.cjs +6 -6
  99. package/dist/components/navigation/index.js +5 -5
  100. package/dist/components/onboarding-guides/index.cjs +6 -6
  101. package/dist/components/onboarding-guides/index.js +5 -5
  102. package/dist/components/tickets/index.cjs +7 -7
  103. package/dist/components/tickets/index.js +6 -6
  104. package/dist/components/ui/data-table/use-data-table.d.ts.map +1 -1
  105. package/dist/components/ui/hover-dropdown.d.ts.map +1 -1
  106. package/dist/components/ui/index.cjs +3 -3
  107. package/dist/components/ui/index.js +2 -2
  108. package/dist/index.cjs +7 -7
  109. package/dist/index.js +6 -6
  110. package/dist/utils/index.cjs.map +1 -1
  111. package/dist/utils/index.js.map +1 -1
  112. package/package.json +1 -2
  113. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +0 -130
  114. package/src/chat-protocol/events.ts +1 -9
  115. package/src/chat-protocol/nats-decoder.ts +0 -7
  116. package/src/components/chat/__tests__/chat-message-list.test.tsx +0 -70
  117. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +0 -86
  118. package/src/components/chat/chat-message-enhanced.tsx +13 -28
  119. package/src/components/chat/chat-message-list.tsx +22 -27
  120. package/src/components/chat/embeddable-chat.tsx +36 -55
  121. package/src/components/chat/entity-cards/dispatch.tsx +17 -86
  122. package/src/components/chat/hooks/use-nats-chat-adapter.ts +0 -7
  123. package/src/components/chat/hooks/use-slash-commands.ts +5 -9
  124. package/src/components/chat/hooks/use-sse-chat-adapter.ts +29 -2
  125. package/src/components/chat/remark-strip-citations.ts +3 -5
  126. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +0 -56
  127. package/src/components/chat/stream/chat-stream-reducer.ts +0 -31
  128. package/src/components/chat/tool-execution-display.tsx +2 -5
  129. package/src/components/chat/types/component.types.ts +0 -4
  130. package/src/components/chat/types/message.types.ts +0 -39
  131. package/src/components/chat/types/unified-chat-state.types.ts +3 -4
  132. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +0 -57
  133. package/src/components/chat/utils/process-historical-messages.ts +1 -19
  134. package/src/components/ui/__tests__/data-table-compiler-identity.test.tsx +74 -0
  135. package/src/components/ui/data-table/use-data-table.ts +36 -1
  136. package/src/components/ui/hover-dropdown.tsx +3 -8
  137. package/dist/chat-protocol/source-metadata.d.ts +0 -5
  138. package/dist/chat-protocol/source-metadata.d.ts.map +0 -1
  139. package/dist/chunk-4MVGFYRE.js.map +0 -1
  140. package/dist/chunk-EKKR5ZFB.cjs.map +0 -1
  141. package/dist/chunk-UYHETUX3.cjs.map +0 -1
  142. package/dist/components/chat/utils/chat-source-metadata.d.ts +0 -5
  143. package/dist/components/chat/utils/chat-source-metadata.d.ts.map +0 -1
  144. package/src/chat-protocol/__tests__/source-metadata.test.ts +0 -74
  145. package/src/chat-protocol/source-metadata.ts +0 -202
  146. package/src/components/chat/__tests__/embeddable-chat-sources.test.tsx +0 -411
  147. package/src/components/chat/__tests__/remark-strip-citations.test.ts +0 -52
  148. package/src/components/chat/__tests__/tool-execution-display.test.tsx +0 -93
  149. package/src/components/chat/entity-cards/__tests__/dispatch-video-refs.test.tsx +0 -74
  150. package/src/components/chat/hooks/__tests__/use-slash-commands.test.ts +0 -64
  151. package/src/components/chat/utils/chat-source-metadata.ts +0 -19
  152. /package/dist/{chunk-RE3KVI2Q.js.map → chunk-3PDTGDOA.js.map} +0 -0
  153. /package/dist/{chunk-5X7AJ67N.js.map → chunk-436G4DI4.js.map} +0 -0
  154. /package/dist/{chunk-7A6J4JTW.js.map → chunk-6BFEJXMI.js.map} +0 -0
  155. /package/dist/{chunk-RQ5PYSRJ.js.map → chunk-BLJEPNMJ.js.map} +0 -0
  156. /package/dist/{chunk-WFWWQO2I.js.map → chunk-F3YRPKUA.js.map} +0 -0
  157. /package/dist/{chunk-FDWGO244.js.map → chunk-LODCKZ72.js.map} +0 -0
  158. /package/dist/{chunk-E2QFZDCP.js.map → chunk-OKD7MX7Y.js.map} +0 -0
  159. /package/dist/{chunk-OW3FUJ2I.js.map → chunk-P24OI2CD.js.map} +0 -0
  160. /package/dist/{chunk-QSO24MEK.js.map → chunk-R2U2D7LU.js.map} +0 -0
  161. /package/dist/{chunk-KQWI25NP.js.map → chunk-SI57FQZ2.js.map} +0 -0
  162. /package/dist/{chunk-JQO47UHC.js.map → chunk-SVR75PFW.js.map} +0 -0
  163. /package/dist/{chunk-OZRT3ZI3.js.map → chunk-V4WUSOGS.js.map} +0 -0
  164. /package/dist/{chunk-UODM3HJC.js.map → chunk-VWNU2PFX.js.map} +0 -0
  165. /package/dist/{chunk-QYDIVL4X.js.map → chunk-YB5DFQKD.js.map} +0 -0
@@ -1,5 +0,0 @@
1
- import type { ChatRef } from '../chat-ref.types';
2
- import type { ChatSource } from '../types/message.types';
3
- export declare function mergeChatSources(current: ChatSource[] | undefined, incoming: ChatSource[]): ChatSource[];
4
- export declare function mergeChatRefs(current: ChatRef[] | undefined, incoming: ChatRef[]): ChatRef[];
5
- //# sourceMappingURL=chat-source-metadata.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"chat-source-metadata.d.ts","sourceRoot":"","sources":["../../../../src/components/chat/utils/chat-source-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAMxG;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAO5F"}
@@ -1,74 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import { isSourceMetadataCardRef, sourceMetadataEvent } from '../source-metadata';
3
-
4
- describe('source metadata card provenance', () => {
5
- it('is recognized by a separate package entrypoint module instance', async () => {
6
- const event = sourceMetadataEvent({
7
- sources: [
8
- {
9
- index: 1,
10
- id: 'install-agent',
11
- name: 'Install the OpenFrame agent on Windows',
12
- path: 'getting-started/install-agent.md',
13
- documentType: 'markdown',
14
- },
15
- ],
16
- cards: [
17
- {
18
- ref: '[card://markdown:install-agent]',
19
- entityType: 'markdown',
20
- entityId: 'install-agent',
21
- },
22
- ],
23
- });
24
- const ref = event?.refs?.[0];
25
- if (!ref) throw new Error('Expected matching source metadata card ref');
26
-
27
- vi.resetModules();
28
- const { isSourceMetadataCardRef: isSourceMetadataCardRefFromSecondEntrypoint } = await import('../source-metadata');
29
-
30
- expect(isSourceMetadataCardRefFromSecondEntrypoint(ref)).toBe(true);
31
- });
32
-
33
- it('keeps source provenance after a serializing host boundary', () => {
34
- const event = sourceMetadataEvent({
35
- sources: [
36
- {
37
- index: 1,
38
- id: 'install-agent',
39
- name: 'Install the OpenFrame agent on Windows',
40
- path: 'getting-started/install-agent.md',
41
- documentType: 'markdown',
42
- },
43
- ],
44
- cards: [
45
- {
46
- ref: '[card://markdown:install-agent]',
47
- entityType: 'markdown',
48
- entityId: 'install-agent',
49
- },
50
- ],
51
- });
52
- const ref = event?.refs?.[0];
53
- if (!ref) throw new Error('Expected matching source metadata card ref');
54
-
55
- const restoredRef = structuredClone(ref);
56
-
57
- expect(isSourceMetadataCardRef(restoredRef)).toBe(true);
58
- });
59
-
60
- it('does not recognize an arbitrary enriched card ref', async () => {
61
- vi.resetModules();
62
- const { isSourceMetadataCardRef: isSourceMetadataCardRefFromSecondEntrypoint } = await import('../source-metadata');
63
-
64
- expect(
65
- isSourceMetadataCardRefFromSecondEntrypoint({
66
- type: 'markdown',
67
- id: 'unmatched-doc',
68
- title: 'Plausible but unmatched document',
69
- url: null,
70
- metadata: { path: 'docs/unmatched.md' },
71
- }),
72
- ).toBe(false);
73
- });
74
- });
@@ -1,202 +0,0 @@
1
- import type { ChatRef } from '../components/chat/chat-ref.types';
2
- import type { ChatSource } from '../components/chat/types/message.types';
3
- import type { SourcesEvent } from './events';
4
-
5
- const CARD_REFERENCE = /^\[card:\/\/([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)\]$/;
6
- const SOURCE_METADATA_MATCHED = 'sourceMetadataMatched';
7
-
8
- export function isSourceMetadataCardRef(ref: ChatRef): boolean {
9
- return ref.metadata?.[SOURCE_METADATA_MATCHED] === true;
10
- }
11
-
12
- function requiredString(value: unknown): string | null {
13
- if (typeof value !== 'string') return null;
14
- const normalized = value.trim();
15
- return normalized ? normalized : null;
16
- }
17
-
18
- function optionalString(value: unknown): string | undefined {
19
- return requiredString(value) ?? undefined;
20
- }
21
-
22
- function httpsUrl(value: unknown): string | undefined {
23
- const candidate = optionalString(value);
24
- if (!candidate) return undefined;
25
- try {
26
- return new URL(candidate).protocol === 'https:' ? candidate : undefined;
27
- } catch {
28
- return undefined;
29
- }
30
- }
31
-
32
- function youtubeId(value: unknown): string | undefined {
33
- const candidate = optionalString(value);
34
- if (!candidate) return undefined;
35
- if (/^[A-Za-z0-9_-]{11}$/.test(candidate)) return candidate;
36
- const url = httpsUrl(candidate);
37
- if (!url) return undefined;
38
- const parsed = new URL(url);
39
- const hostname = parsed.hostname.toLowerCase();
40
- const youtubeHost = hostname === 'youtu.be' || hostname === 'youtube.com' || hostname.endsWith('.youtube.com');
41
- if (!youtubeHost) return undefined;
42
- const pathId =
43
- hostname === 'youtu.be'
44
- ? parsed.pathname.split('/').filter(Boolean)[0]
45
- : parsed.pathname.match(/^\/(?:embed|v|shorts)\/([A-Za-z0-9_-]{11})(?:\/|$)/)?.[1];
46
- const id = pathId ?? parsed.searchParams.get('v') ?? undefined;
47
- return id && /^[A-Za-z0-9_-]{11}$/.test(id) ? id : undefined;
48
- }
49
-
50
- function nullableString(value: unknown): string | null | undefined {
51
- return value === null ? null : optionalString(value);
52
- }
53
-
54
- function normalizeSourceItems(value: unknown): NonNullable<ChatSource['items']> | undefined {
55
- if (!Array.isArray(value)) return undefined;
56
- const items = value.flatMap(candidate => {
57
- if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return [];
58
- const item = candidate as Record<string, unknown>;
59
- const id = requiredString(item.id);
60
- const documentType = requiredString(item.documentType);
61
- const name = requiredString(item.name);
62
- if (!id || !documentType || !name) return [];
63
- const externalUrl = httpsUrl(item.externalUrl);
64
- const targetPlatform = nullableString(item.targetPlatform);
65
- const path = nullableString(item.path);
66
- return [
67
- {
68
- id,
69
- documentType,
70
- name,
71
- ...(externalUrl ? { externalUrl } : {}),
72
- ...(targetPlatform !== undefined ? { targetPlatform } : {}),
73
- ...(path !== undefined ? { path } : {}),
74
- },
75
- ];
76
- });
77
- return items.length > 0 ? items : undefined;
78
- }
79
-
80
- function normalizeSources(value: unknown): ChatSource[] {
81
- if (!Array.isArray(value)) return [];
82
- const seen = new Set<number>();
83
- const sources: ChatSource[] = [];
84
- for (const candidate of value) {
85
- if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) continue;
86
- const source = candidate as Record<string, unknown>;
87
- const index = source.index;
88
- const name = requiredString(source.name);
89
- const path = requiredString(source.path);
90
- const documentType = requiredString(source.documentType);
91
- const validIndex =
92
- typeof index === 'number' && Number.isFinite(index) && Number.isInteger(index) && index > 0 && !seen.has(index);
93
- if (!validIndex || !name || !path || !documentType) continue;
94
- seen.add(index);
95
- const externalUrl = httpsUrl(source.externalUrl);
96
- const targetPlatform = nullableString(source.targetPlatform);
97
- const id = optionalString(source.id);
98
- const sourceRepo = optionalString(source.sourceRepo);
99
- const label = optionalString(source.label);
100
- const items = normalizeSourceItems(source.items);
101
- sources.push({
102
- index,
103
- name,
104
- path,
105
- documentType,
106
- ...(externalUrl ? { externalUrl } : {}),
107
- ...(targetPlatform !== undefined ? { targetPlatform } : {}),
108
- ...(id ? { id } : {}),
109
- ...(sourceRepo ? { sourceRepo } : {}),
110
- ...(label ? { label } : {}),
111
- ...(items ? { items } : {}),
112
- });
113
- }
114
- return sources;
115
- }
116
-
117
- function normalizeVideoMetadata(value: unknown): Record<string, string> {
118
- if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
119
- const input = value as Record<string, unknown>;
120
- const metadata: Record<string, string> = {};
121
- for (const key of ['videoUrl', 'highlightVideoUrl', 'videoPoster', 'highlightVideoPoster'] as const) {
122
- const url = httpsUrl(input[key]);
123
- if (url) metadata[key] = url;
124
- }
125
- const id = youtubeId(input.youtubeUrl);
126
- if (id) metadata.youtubeUrl = id;
127
- return metadata;
128
- }
129
-
130
- function normalizeVideos(value: unknown): ChatRef[] {
131
- if (!Array.isArray(value)) return [];
132
- return value.flatMap(candidate => {
133
- if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return [];
134
- const video = candidate as Record<string, unknown>;
135
- const match = typeof video.ref === 'string' ? CARD_REFERENCE.exec(video.ref) : null;
136
- if (!match || match[1] !== 'video') return [];
137
- const id = match[2];
138
- const title = optionalString(video.title) ?? id;
139
- const url = httpsUrl(video.url) ?? null;
140
- const sourceRepo = optionalString(video.sourceRepo);
141
- const metadata = normalizeVideoMetadata(video.metadata);
142
- if (url) {
143
- const idFromUrl = youtubeId(url);
144
- if (idFromUrl) {
145
- metadata.youtubeUrl = idFromUrl;
146
- delete metadata.videoUrl;
147
- } else {
148
- metadata.videoUrl = url;
149
- delete metadata.youtubeUrl;
150
- }
151
- }
152
- return [
153
- {
154
- type: 'video',
155
- id,
156
- title,
157
- url,
158
- ...(sourceRepo ? { sourceRepo } : {}),
159
- ...(Object.keys(metadata).length > 0 ? { metadata } : {}),
160
- },
161
- ];
162
- });
163
- }
164
-
165
- function normalizeCards(value: unknown, sources: ChatSource[]): ChatRef[] {
166
- if (!Array.isArray(value)) return [];
167
- return value.flatMap(candidate => {
168
- if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return [];
169
- const card = candidate as Record<string, unknown>;
170
- const match = typeof card.ref === 'string' ? CARD_REFERENCE.exec(card.ref) : null;
171
- if (!match || match[1] === 'video') return [];
172
- const type = requiredString(card.entityType);
173
- const id = requiredString(card.entityId);
174
- if (!type || !id || type !== match[1] || id !== match[2]) return [];
175
- const source = sources.find(item => item.id === id && item.documentType === type);
176
- const ref: ChatRef = {
177
- type,
178
- id,
179
- title: source?.name ?? id,
180
- url: source?.externalUrl ?? null,
181
- ...(source?.sourceRepo ? { sourceRepo: source.sourceRepo } : {}),
182
- ...(source?.targetPlatform !== undefined ? { targetPlatform: source.targetPlatform } : {}),
183
- ...(source?.path ? { metadata: { path: source.path, [SOURCE_METADATA_MATCHED]: true } } : {}),
184
- };
185
- return [ref];
186
- });
187
- }
188
-
189
- export function sourceMetadataEvent(payload: unknown): SourcesEvent | null {
190
- if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null;
191
- const metadata = payload as Record<string, unknown>;
192
- const sources = normalizeSources(metadata.sources);
193
- const refs = [...normalizeVideos(metadata.videos), ...normalizeCards(metadata.cards, sources)];
194
- const refsByIdentity = new Map<string, ChatRef>();
195
- refs.forEach(ref => {
196
- const identity = `${ref.type}:${ref.id}`;
197
- if (!refsByIdentity.has(identity)) refsByIdentity.set(identity, ref);
198
- });
199
- const uniqueRefs = Array.from(refsByIdentity.values());
200
- if (sources.length === 0 && uniqueRefs.length === 0) return null;
201
- return { type: 'sources', sources, ...(uniqueRefs.length > 0 ? { refs: uniqueRefs } : {}) };
202
- }
@@ -1,411 +0,0 @@
1
- import * as DialogPrimitive from '@radix-ui/react-dialog';
2
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
- import { fireEvent, render, screen } from '@testing-library/react';
4
- import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
5
- import { decodeNatsChunk } from '../../../chat-protocol/nats-decoder';
6
- import { type ChatRuntime, ChatRuntimeContext } from '../../../contexts/chat-runtime-context';
7
- import { EmbeddableChat } from '../embeddable-chat';
8
- import { createChatStreamReducer } from '../stream/chat-stream-reducer';
9
- import type { UnifiedChatMessage, UnifiedChatState } from '../types/unified-chat-state.types';
10
-
11
- vi.mock('@mux/mux-player-react', () => ({
12
- default: ({ src }: { src?: string }) => <div data-testid="mux-player" data-src={src} />,
13
- }));
14
-
15
- const runtime: ChatRuntime = {
16
- endpoints: {
17
- chatStreamUrl: '/test/chat',
18
- approvalToolUrl: '/test/approve',
19
- commandsUrl: '/test/commands',
20
- buildListUrl: () => null,
21
- attachmentUploadUrl: '/test/upload',
22
- attachmentViewUrlPrefix: '/test/view/',
23
- identityUrl: '/test/identity',
24
- },
25
- navigation: { mode: 'host' },
26
- source: 'openframe',
27
- };
28
-
29
- function stateFor(messages: UnifiedChatMessage[]): UnifiedChatState {
30
- const noop = () => {};
31
- const asyncNoop = async () => {};
32
- return {
33
- messages,
34
- isLoading: false,
35
- streamingPhase: 'idle',
36
- sendMessage: asyncNoop,
37
- stopMessage: noop,
38
- clearMessages: noop,
39
- discussRef: noop,
40
- displayRef: noop,
41
- currentProvider: null,
42
- currentModelLabel: null,
43
- currentContextWindowMaxTokens: null,
44
- currentInputTokens: null,
45
- currentOutputTokens: null,
46
- currentCacheHitRatePct: null,
47
- currentUsageBreakdown: null,
48
- dialogs: [{ id: 'dialog-1', title: 'Sources' }],
49
- activeDialogId: 'dialog-1',
50
- selectDialog: noop,
51
- startNewDialog: () => Promise.resolve(null),
52
- deleteDialog: asyncNoop,
53
- renameDialog: asyncNoop,
54
- archiveDialog: asyncNoop,
55
- isDialogsLoading: false,
56
- dialogsError: false,
57
- reloadDialogs: noop,
58
- isMessagesLoading: false,
59
- hasMoreDialogs: false,
60
- loadMoreDialogs: asyncNoop,
61
- hasMoreMessages: false,
62
- loadMoreMessages: asyncNoop,
63
- approveRequest: asyncNoop,
64
- rejectRequest: asyncNoop,
65
- dialogTokenUsage: null,
66
- connectionState: 'connected',
67
- };
68
- }
69
-
70
- function renderChat(messages: UnifiedChatMessage[]) {
71
- const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
72
- return render(
73
- <QueryClientProvider client={queryClient}>
74
- <ChatRuntimeContext.Provider value={runtime}>
75
- <DialogPrimitive.Root open>
76
- <EmbeddableChat
77
- shell="none"
78
- defaultOpen
79
- defaultActiveMode="mingo"
80
- showInternalTrigger={false}
81
- previewMode
82
- mingoState={stateFor(messages)}
83
- />
84
- </DialogPrimitive.Root>
85
- </ChatRuntimeContext.Provider>
86
- </QueryClientProvider>,
87
- );
88
- }
89
-
90
- beforeAll(() => {
91
- vi.stubGlobal('matchMedia', () => ({
92
- matches: false,
93
- addEventListener: vi.fn(),
94
- removeEventListener: vi.fn(),
95
- }));
96
- vi.stubGlobal(
97
- 'ResizeObserver',
98
- class {
99
- observe(): void {}
100
- disconnect(): void {}
101
- unobserve(): void {}
102
- },
103
- );
104
- vi.spyOn(globalThis, 'fetch').mockResolvedValue(
105
- new Response(JSON.stringify({ commands: [] }), {
106
- status: 200,
107
- headers: { 'Content-Type': 'application/json' },
108
- }),
109
- );
110
- });
111
-
112
- afterAll(() => {
113
- vi.restoreAllMocks();
114
- vi.unstubAllGlobals();
115
- });
116
-
117
- describe('EmbeddableChat source strip', () => {
118
- it('keeps sources attached to their assistant turn after later messages', () => {
119
- renderChat([
120
- {
121
- id: 'assistant-1',
122
- role: 'assistant',
123
- content: 'Use the first guide [1].',
124
- sources: [
125
- {
126
- index: 1,
127
- name: 'First turn source',
128
- path: 'docs/first',
129
- documentType: 'docs',
130
- },
131
- ],
132
- },
133
- {
134
- id: 'assistant-2',
135
- role: 'assistant',
136
- content: 'Use the second guide [1].',
137
- sources: [
138
- {
139
- index: 1,
140
- name: 'Second turn source',
141
- path: 'docs/second',
142
- documentType: 'docs',
143
- },
144
- ],
145
- },
146
- {
147
- id: 'assistant-3',
148
- role: 'assistant',
149
- content: 'Here is a plain follow-up answer.',
150
- },
151
- ]);
152
-
153
- expect(screen.getByText('[1] First turn source')).toBeInTheDocument();
154
- expect(screen.getByText('[1] Second turn source')).toBeInTheDocument();
155
- });
156
-
157
- it('renders live SOURCES metadata in citation order with uncited overflow', () => {
158
- const reducer = createChatStreamReducer({ transport: 'nats' });
159
- for (const chunk of [
160
- { type: 'MESSAGE_START', streamSeq: 1 },
161
- {
162
- type: 'SOURCES',
163
- payload: {
164
- sources: [
165
- { index: 1, name: 'First source', path: 'first', documentType: 'docs' },
166
- { index: 2, name: 'Second source', path: 'second', documentType: 'docs' },
167
- { index: 3, name: 'Third source', path: 'third', documentType: 'docs' },
168
- { index: 4, name: 'Fourth source', path: 'fourth', documentType: 'docs' },
169
- ],
170
- videos: [
171
- {
172
- ref: '[card://video:mux-9b6586b494]',
173
- title: 'Product overview',
174
- url: 'https://stream.mux.com/product-overview.m3u8',
175
- },
176
- ],
177
- },
178
- streamSeq: 2,
179
- },
180
- {
181
- type: 'TEXT',
182
- text:
183
- 'Compare the third source [3] with the first [1] and cite [3] once. ' +
184
- 'Watch [card://video:mux-9b6586b494].',
185
- streamSeq: 3,
186
- },
187
- { type: 'MESSAGE_END', streamSeq: 4 },
188
- ]) {
189
- const event = decodeNatsChunk(chunk);
190
- if (event) reducer.apply(event);
191
- }
192
-
193
- renderChat(reducer.state.messages);
194
-
195
- const chips = screen.getAllByTitle(/source$/i);
196
- expect(chips.map(chip => chip.textContent)).toEqual(['[3] Third source', '[1] First source']);
197
- expect(screen.queryByText('[2] Second source')).not.toBeInTheDocument();
198
- const expander = screen.getByRole('button', { name: 'Show 2 additional retrieved sources' });
199
- expect(expander).toHaveTextContent('+2 more retrieved sources');
200
-
201
- fireEvent.click(expander);
202
-
203
- expect(screen.getByText('[2] Second source')).toBeInTheDocument();
204
- expect(screen.getByText('[4] Fourth source')).toBeInTheDocument();
205
- expect(screen.getByTestId('mux-player')).toHaveAttribute(
206
- 'data-src',
207
- 'https://stream.mux.com/product-overview.m3u8',
208
- );
209
- });
210
-
211
- it('keeps every uncited source accessible behind the top-three fallback', () => {
212
- const reducer = createChatStreamReducer({ transport: 'nats' });
213
- for (const chunk of [
214
- { type: 'MESSAGE_START', streamSeq: 1 },
215
- {
216
- type: 'SOURCES',
217
- payload: {
218
- sources: Array.from({ length: 5 }, (_, index) => ({
219
- index: index + 1,
220
- name: `Source ${index + 1}`,
221
- path: `source-${index + 1}`,
222
- documentType: 'docs',
223
- })),
224
- },
225
- streamSeq: 2,
226
- },
227
- { type: 'TEXT', text: 'Here are the relevant materials.', streamSeq: 3 },
228
- { type: 'MESSAGE_END', streamSeq: 4 },
229
- ]) {
230
- const event = decodeNatsChunk(chunk);
231
- if (event) reducer.apply(event);
232
- }
233
-
234
- renderChat(reducer.state.messages);
235
-
236
- expect(screen.getByText('[1] Source 1')).toBeInTheDocument();
237
- expect(screen.getByText('[3] Source 3')).toBeInTheDocument();
238
- expect(screen.queryByText('[4] Source 4')).not.toBeInTheDocument();
239
-
240
- const expander = screen.getByRole('button', { name: 'Show 2 additional retrieved sources' });
241
- fireEvent.click(expander);
242
-
243
- expect(screen.getByText('[4] Source 4')).toBeInTheDocument();
244
- expect(screen.getByText('[5] Source 5')).toBeInTheDocument();
245
- });
246
-
247
- it('renders a markdown card from matching source metadata when hydration is unavailable', () => {
248
- const reducer = createChatStreamReducer({ transport: 'nats' });
249
- for (const chunk of [
250
- { type: 'MESSAGE_START', streamSeq: 1 },
251
- {
252
- type: 'SOURCES',
253
- payload: {
254
- sources: [
255
- {
256
- index: 1,
257
- id: 'install-agent',
258
- name: 'Install the OpenFrame agent on Windows',
259
- path: 'getting-started/install-agent.md',
260
- documentType: 'markdown',
261
- sourceRepo: 'openframe-docs',
262
- },
263
- ],
264
- cards: [
265
- {
266
- ref: '[card://markdown:install-agent]',
267
- entityType: 'markdown',
268
- entityId: 'install-agent',
269
- },
270
- ],
271
- },
272
- streamSeq: 2,
273
- },
274
- {
275
- type: 'TEXT',
276
- text: 'Follow [card://markdown:install-agent].',
277
- streamSeq: 3,
278
- },
279
- { type: 'MESSAGE_END', streamSeq: 4 },
280
- ]) {
281
- const event = decodeNatsChunk(chunk);
282
- if (event) reducer.apply(event);
283
- }
284
-
285
- renderChat(reducer.state.messages);
286
-
287
- expect(screen.getByText('Install the OpenFrame agent on Windows')).toBeInTheDocument();
288
- expect(screen.queryByText('FAILED')).not.toBeInTheDocument();
289
- });
290
-
291
- it('renders a matched source card when its display name equals its id', () => {
292
- const reducer = createChatStreamReducer({ transport: 'nats' });
293
- for (const chunk of [
294
- { type: 'MESSAGE_START', streamSeq: 1 },
295
- {
296
- type: 'SOURCES',
297
- payload: {
298
- sources: [
299
- {
300
- index: 1,
301
- id: 'install-agent',
302
- name: 'install-agent',
303
- path: 'getting-started/install-agent.md',
304
- documentType: 'markdown',
305
- },
306
- ],
307
- cards: [
308
- {
309
- ref: '[card://markdown:install-agent]',
310
- entityType: 'markdown',
311
- entityId: 'install-agent',
312
- },
313
- ],
314
- },
315
- streamSeq: 2,
316
- },
317
- { type: 'TEXT', text: 'Follow [card://markdown:install-agent].', streamSeq: 3 },
318
- { type: 'MESSAGE_END', streamSeq: 4 },
319
- ]) {
320
- const event = decodeNatsChunk(chunk);
321
- if (event) reducer.apply(event);
322
- }
323
-
324
- renderChat(reducer.state.messages);
325
-
326
- expect(screen.getAllByText('install-agent').length).toBeGreaterThan(0);
327
- expect(screen.queryByText('FAILED')).not.toBeInTheDocument();
328
- });
329
-
330
- it('does not treat an arbitrary enriched ref as matched source metadata', () => {
331
- renderChat([
332
- {
333
- id: 'assistant-1',
334
- role: 'assistant',
335
- content: 'Follow [card://markdown:unmatched-doc].',
336
- refs: [
337
- {
338
- type: 'markdown',
339
- id: 'unmatched-doc',
340
- title: 'Plausible but unmatched document',
341
- url: null,
342
- metadata: { path: 'docs/unmatched.md' },
343
- },
344
- ],
345
- },
346
- ]);
347
-
348
- expect(screen.getByText('FAILED')).toBeInTheDocument();
349
- expect(screen.queryByText('Plausible but unmatched document')).not.toBeInTheDocument();
350
- });
351
-
352
- it('opens every item carried by a grouped source', () => {
353
- const reducer = createChatStreamReducer({ transport: 'nats' });
354
- for (const chunk of [
355
- { type: 'MESSAGE_START', streamSeq: 1 },
356
- {
357
- type: 'SOURCES',
358
- payload: {
359
- sources: [
360
- {
361
- index: 1,
362
- name: 'Case Studies (3 records)',
363
- path: 'case-studies',
364
- documentType: 'case-study',
365
- items: [
366
- {
367
- id: 'case-study-1',
368
- name: 'Northstar MSP',
369
- path: 'case-studies/northstar',
370
- documentType: 'case-study',
371
- },
372
- {
373
- id: 'case-study-2',
374
- name: 'Blue Harbor IT',
375
- path: 'case-studies/blue-harbor',
376
- documentType: 'case-study',
377
- },
378
- {
379
- id: 'case-study-3',
380
- name: 'Summit Technology',
381
- path: 'case-studies/summit',
382
- documentType: 'case-study',
383
- },
384
- ],
385
- },
386
- ],
387
- },
388
- streamSeq: 2,
389
- },
390
- { type: 'TEXT', text: 'Here are three customer stories [1].', streamSeq: 3 },
391
- { type: 'MESSAGE_END', streamSeq: 4 },
392
- ]) {
393
- const event = decodeNatsChunk(chunk);
394
- if (event) reducer.apply(event);
395
- }
396
-
397
- renderChat(reducer.state.messages);
398
-
399
- const groupedSource = screen.getByText('[1] Case Studies (3 records)');
400
- fireEvent.mouseEnter(groupedSource);
401
- fireEvent.click(groupedSource);
402
-
403
- expect(screen.getByText('Northstar MSP')).toBeInTheDocument();
404
- expect(screen.getByText('Blue Harbor IT')).toBeInTheDocument();
405
- expect(screen.getByText('Summit Technology')).toBeInTheDocument();
406
-
407
- fireEvent.click(groupedSource);
408
-
409
- expect(screen.queryByText('Northstar MSP')).not.toBeInTheDocument();
410
- });
411
- });