@flamingo-stack/openframe-frontend-core 0.0.588 → 0.0.589-1973.5571.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.
- package/dist/chat-protocol/events.d.ts +8 -2
- package/dist/chat-protocol/events.d.ts.map +1 -1
- package/dist/chat-protocol/index.cjs +188 -0
- package/dist/chat-protocol/index.cjs.map +1 -1
- package/dist/chat-protocol/index.js +188 -0
- package/dist/chat-protocol/index.js.map +1 -1
- package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
- package/dist/chat-protocol/source-metadata.d.ts +5 -0
- package/dist/chat-protocol/source-metadata.d.ts.map +1 -0
- package/dist/{chunk-GKPY7VLK.js → chunk-47NIV4FC.js} +4 -4
- package/dist/{chunk-D7YISHXC.js → chunk-4MVGFYRE.js} +534 -190
- package/dist/chunk-4MVGFYRE.js.map +1 -0
- package/dist/{chunk-I5CJJFEJ.js → chunk-4VDGIODG.js} +3 -3
- package/dist/{chunk-PAUJQV3C.cjs → chunk-627JVTID.cjs} +15 -15
- package/dist/{chunk-PAUJQV3C.cjs.map → chunk-627JVTID.cjs.map} +1 -1
- package/dist/{chunk-3OCYNMZ5.js → chunk-CQNILR7U.js} +3 -3
- package/dist/{chunk-BONXSRHZ.cjs → chunk-DPQGO4PH.cjs} +21 -21
- package/dist/{chunk-BONXSRHZ.cjs.map → chunk-DPQGO4PH.cjs.map} +1 -1
- package/dist/{chunk-A6X7EXP6.cjs → chunk-DVAAXDZV.cjs} +10 -10
- package/dist/{chunk-A6X7EXP6.cjs.map → chunk-DVAAXDZV.cjs.map} +1 -1
- package/dist/{chunk-CN36E4PK.js → chunk-EJEHIAOV.js} +2 -2
- package/dist/{chunk-G45RGWHF.cjs → chunk-EKKR5ZFB.cjs} +887 -543
- package/dist/chunk-EKKR5ZFB.cjs.map +1 -0
- package/dist/{chunk-IYDHV2CS.cjs → chunk-FIEOWSXH.cjs} +43 -43
- package/dist/{chunk-IYDHV2CS.cjs.map → chunk-FIEOWSXH.cjs.map} +1 -1
- package/dist/{chunk-CJOWLRUU.js → chunk-G4ND6ZZK.js} +2 -2
- package/dist/{chunk-KWTV3KGY.cjs → chunk-GDHQTVBP.cjs} +9 -9
- package/dist/{chunk-KWTV3KGY.cjs.map → chunk-GDHQTVBP.cjs.map} +1 -1
- package/dist/{chunk-ADANP3ZA.cjs → chunk-GOHHFX4O.cjs} +33 -33
- package/dist/{chunk-ADANP3ZA.cjs.map → chunk-GOHHFX4O.cjs.map} +1 -1
- package/dist/{chunk-KVNGYFFP.js → chunk-GU5TWGAJ.js} +3 -3
- package/dist/{chunk-EGYX55HT.cjs → chunk-HQKWJFFX.cjs} +159 -159
- package/dist/{chunk-EGYX55HT.cjs.map → chunk-HQKWJFFX.cjs.map} +1 -1
- package/dist/{chunk-NXZC6UTN.js → chunk-HZ6VUTLA.js} +2 -2
- package/dist/{chunk-WUCYI3BC.cjs → chunk-KH5KDKCE.cjs} +24 -24
- package/dist/{chunk-WUCYI3BC.cjs.map → chunk-KH5KDKCE.cjs.map} +1 -1
- package/dist/{chunk-JDINUWND.cjs → chunk-MQFA5DXU.cjs} +69 -69
- package/dist/{chunk-JDINUWND.cjs.map → chunk-MQFA5DXU.cjs.map} +1 -1
- package/dist/{chunk-5CHW7AMP.cjs → chunk-NHW2AM2X.cjs} +10 -10
- package/dist/{chunk-5CHW7AMP.cjs.map → chunk-NHW2AM2X.cjs.map} +1 -1
- package/dist/{chunk-KK3XU6YJ.cjs → chunk-NIHPN72N.cjs} +4 -4
- package/dist/{chunk-KK3XU6YJ.cjs.map → chunk-NIHPN72N.cjs.map} +1 -1
- package/dist/{chunk-EGUU3QCU.cjs → chunk-NNRRRWCD.cjs} +3 -3
- package/dist/{chunk-EGUU3QCU.cjs.map → chunk-NNRRRWCD.cjs.map} +1 -1
- package/dist/{chunk-4ZKAMPBX.js → chunk-OAY342OE.js} +2 -2
- package/dist/{chunk-TOYHZHTK.js → chunk-PDFH7CXB.js} +2 -2
- package/dist/{chunk-NIDPX52C.js → chunk-QIHBS5PM.js} +5 -5
- package/dist/{chunk-CNKVEVUN.js → chunk-SPOIDC23.js} +5 -5
- package/dist/{chunk-MENQLD57.cjs → chunk-T5FJOWHA.cjs} +5 -5
- package/dist/{chunk-MENQLD57.cjs.map → chunk-T5FJOWHA.cjs.map} +1 -1
- package/dist/{chunk-5FNWOF46.js → chunk-WZZSFOWG.js} +6 -6
- package/dist/{chunk-FVRRYJ6P.cjs → chunk-XIKSZSHC.cjs} +22 -22
- package/dist/{chunk-FVRRYJ6P.cjs.map → chunk-XIKSZSHC.cjs.map} +1 -1
- package/dist/{chunk-C6RJRSIM.cjs → chunk-XTMNO7MC.cjs} +3 -3
- package/dist/{chunk-C6RJRSIM.cjs.map → chunk-XTMNO7MC.cjs.map} +1 -1
- package/dist/{chunk-FQBR6SCA.js → chunk-XW4GOMK6.js} +3 -3
- package/dist/{chunk-77Y7R243.js → chunk-YEEMXD25.js} +5 -5
- package/dist/{chunk-LGBNHWDN.js → chunk-YTVTZ7JO.js} +3 -3
- package/dist/components/case-studies/index.cjs +9 -9
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +4 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-slash-commands.d.ts +3 -3
- package/dist/components/chat/hooks/use-slash-commands.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +2 -28
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/chat/remark-strip-citations.d.ts.map +1 -1
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
- package/dist/components/chat/tool-execution-display.d.ts.map +1 -1
- package/dist/components/chat/types/component.types.d.ts +4 -0
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +35 -1
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +3 -3
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- package/dist/components/chat/utils/chat-source-metadata.d.ts +5 -0
- package/dist/components/chat/utils/chat-source-metadata.d.ts.map +1 -0
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +4 -4
- package/dist/components/faq/index.js +3 -3
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +36 -36
- package/dist/components/index.js +13 -13
- package/dist/components/meeting-scheduler/index.cjs +21 -21
- package/dist/components/meeting-scheduler/index.js +2 -2
- package/dist/components/navigation/index.cjs +6 -6
- package/dist/components/navigation/index.js +5 -5
- package/dist/components/onboarding-guides/index.cjs +6 -6
- package/dist/components/onboarding-guides/index.js +5 -5
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/hover-dropdown.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/index.cjs +7 -7
- package/dist/index.js +6 -6
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +2 -1
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +130 -0
- package/src/chat-protocol/__tests__/source-metadata.test.ts +74 -0
- package/src/chat-protocol/events.ts +9 -1
- package/src/chat-protocol/nats-decoder.ts +7 -0
- package/src/chat-protocol/source-metadata.ts +202 -0
- package/src/components/chat/__tests__/chat-message-list.test.tsx +70 -0
- package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +86 -0
- package/src/components/chat/__tests__/embeddable-chat-sources.test.tsx +411 -0
- package/src/components/chat/__tests__/remark-strip-citations.test.ts +52 -0
- package/src/components/chat/__tests__/tool-execution-display.test.tsx +93 -0
- package/src/components/chat/chat-message-enhanced.tsx +28 -13
- package/src/components/chat/chat-message-list.tsx +27 -22
- package/src/components/chat/embeddable-chat.tsx +55 -36
- package/src/components/chat/entity-cards/__tests__/dispatch-video-refs.test.tsx +74 -0
- package/src/components/chat/entity-cards/dispatch.tsx +86 -17
- package/src/components/chat/hooks/__tests__/use-slash-commands.test.ts +64 -0
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +7 -0
- package/src/components/chat/hooks/use-slash-commands.ts +9 -5
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +2 -29
- package/src/components/chat/remark-strip-citations.ts +5 -3
- package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +56 -0
- package/src/components/chat/stream/chat-stream-reducer.ts +31 -0
- package/src/components/chat/tool-execution-display.tsx +5 -2
- package/src/components/chat/types/component.types.ts +4 -0
- package/src/components/chat/types/message.types.ts +39 -0
- package/src/components/chat/types/unified-chat-state.types.ts +4 -3
- package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +57 -0
- package/src/components/chat/utils/chat-source-metadata.ts +19 -0
- package/src/components/chat/utils/process-historical-messages.ts +19 -1
- package/src/components/ui/hover-dropdown.tsx +8 -3
- package/dist/chunk-D7YISHXC.js.map +0 -1
- package/dist/chunk-G45RGWHF.cjs.map +0 -1
- /package/dist/{chunk-GKPY7VLK.js.map → chunk-47NIV4FC.js.map} +0 -0
- /package/dist/{chunk-I5CJJFEJ.js.map → chunk-4VDGIODG.js.map} +0 -0
- /package/dist/{chunk-3OCYNMZ5.js.map → chunk-CQNILR7U.js.map} +0 -0
- /package/dist/{chunk-CN36E4PK.js.map → chunk-EJEHIAOV.js.map} +0 -0
- /package/dist/{chunk-CJOWLRUU.js.map → chunk-G4ND6ZZK.js.map} +0 -0
- /package/dist/{chunk-KVNGYFFP.js.map → chunk-GU5TWGAJ.js.map} +0 -0
- /package/dist/{chunk-NXZC6UTN.js.map → chunk-HZ6VUTLA.js.map} +0 -0
- /package/dist/{chunk-4ZKAMPBX.js.map → chunk-OAY342OE.js.map} +0 -0
- /package/dist/{chunk-TOYHZHTK.js.map → chunk-PDFH7CXB.js.map} +0 -0
- /package/dist/{chunk-NIDPX52C.js.map → chunk-QIHBS5PM.js.map} +0 -0
- /package/dist/{chunk-CNKVEVUN.js.map → chunk-SPOIDC23.js.map} +0 -0
- /package/dist/{chunk-5FNWOF46.js.map → chunk-WZZSFOWG.js.map} +0 -0
- /package/dist/{chunk-FQBR6SCA.js.map → chunk-XW4GOMK6.js.map} +0 -0
- /package/dist/{chunk-77Y7R243.js.map → chunk-YEEMXD25.js.map} +0 -0
- /package/dist/{chunk-LGBNHWDN.js.map → chunk-YTVTZ7JO.js.map} +0 -0
|
@@ -0,0 +1,411 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Root } from 'mdast';
|
|
2
|
+
import { unified } from 'unified';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { remarkStripCitations } from '../remark-strip-citations';
|
|
5
|
+
|
|
6
|
+
describe('remarkStripCitations', () => {
|
|
7
|
+
it('removes a citation-only list without leaving broken punctuation', () => {
|
|
8
|
+
const tree: Root = {
|
|
9
|
+
type: 'root',
|
|
10
|
+
children: [
|
|
11
|
+
{
|
|
12
|
+
type: 'paragraph',
|
|
13
|
+
children: [{ type: 'text', value: 'Sources [1], [2], and [3] are cited.' }],
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const result = unified().use(remarkStripCitations).runSync(tree);
|
|
19
|
+
|
|
20
|
+
expect(result).toMatchObject({
|
|
21
|
+
children: [
|
|
22
|
+
{
|
|
23
|
+
children: [{ type: 'text', value: 'Sources are cited.' }],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('keeps prose punctuation while removing individual citations', () => {
|
|
30
|
+
const tree: Root = {
|
|
31
|
+
type: 'root',
|
|
32
|
+
children: [
|
|
33
|
+
{
|
|
34
|
+
type: 'paragraph',
|
|
35
|
+
children: [
|
|
36
|
+
{ type: 'text', value: 'Install on Windows [1], verify the service [2], and confirm Devices [3].' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const result = unified().use(remarkStripCitations).runSync(tree);
|
|
43
|
+
|
|
44
|
+
expect(result).toMatchObject({
|
|
45
|
+
children: [
|
|
46
|
+
{
|
|
47
|
+
children: [{ type: 'text', value: 'Install on Windows, verify the service, and confirm Devices.' }],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { decodeNatsChunk } from '../../../chat-protocol/nats-decoder';
|
|
4
|
+
import { createChatStreamReducer } from '../stream/chat-stream-reducer';
|
|
5
|
+
import { ToolExecutionDisplay } from '../tool-execution-display';
|
|
6
|
+
import type { ToolExecutionData } from '../types';
|
|
7
|
+
|
|
8
|
+
const execution = (overrides: Partial<ToolExecutionData>): ToolExecutionData => ({
|
|
9
|
+
type: 'EXECUTING_TOOL',
|
|
10
|
+
integratedToolType: 'OPENFRAME',
|
|
11
|
+
toolFunction: 'search_docs',
|
|
12
|
+
...overrides,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
vi.stubGlobal(
|
|
17
|
+
'ResizeObserver',
|
|
18
|
+
class {
|
|
19
|
+
observe(): void {}
|
|
20
|
+
disconnect(): void {}
|
|
21
|
+
unobserve(): void {}
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterAll(() => {
|
|
27
|
+
vi.unstubAllGlobals();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('ToolExecutionDisplay remote read activity', () => {
|
|
31
|
+
it('keeps a completed remote read concise through the real wire pipeline', () => {
|
|
32
|
+
const reducer = createChatStreamReducer({ transport: 'nats' });
|
|
33
|
+
for (const chunk of [
|
|
34
|
+
{
|
|
35
|
+
type: 'EXECUTING_TOOL',
|
|
36
|
+
integratedToolType: null,
|
|
37
|
+
toolFunction: 'remote_read',
|
|
38
|
+
title: 'Searching product documentation',
|
|
39
|
+
parameters: null,
|
|
40
|
+
toolExecutionRequestId: 'remote-read-1',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: 'EXECUTED_TOOL',
|
|
44
|
+
integratedToolType: null,
|
|
45
|
+
toolFunction: 'remote_read',
|
|
46
|
+
result: null,
|
|
47
|
+
success: true,
|
|
48
|
+
toolExecutionRequestId: 'remote-read-1',
|
|
49
|
+
},
|
|
50
|
+
]) {
|
|
51
|
+
const event = decodeNatsChunk(chunk);
|
|
52
|
+
if (event) reducer.apply(event);
|
|
53
|
+
}
|
|
54
|
+
const toolSegment = reducer.state.messages
|
|
55
|
+
.flatMap(message => message.segments ?? [])
|
|
56
|
+
.find(segment => segment.type === 'tool_execution');
|
|
57
|
+
|
|
58
|
+
expect(toolSegment?.type).toBe('tool_execution');
|
|
59
|
+
if (toolSegment?.type !== 'tool_execution') return;
|
|
60
|
+
render(<ToolExecutionDisplay message={toolSegment.data} />);
|
|
61
|
+
|
|
62
|
+
expect(screen.getByText('Searching product documentation')).toBeInTheDocument();
|
|
63
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
64
|
+
expect(screen.queryByText('Remote read')).not.toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('keeps a typed local tool expandable when its body is absent', () => {
|
|
68
|
+
render(
|
|
69
|
+
<ToolExecutionDisplay
|
|
70
|
+
message={execution({
|
|
71
|
+
integratedToolType: 'OPENFRAME',
|
|
72
|
+
toolTitle: 'Refresh device inventory',
|
|
73
|
+
})}
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('keeps a backend-described remote read concise while running', () => {
|
|
81
|
+
render(
|
|
82
|
+
<ToolExecutionDisplay
|
|
83
|
+
message={execution({
|
|
84
|
+
integratedToolType: undefined,
|
|
85
|
+
toolTitle: 'Searching product documentation',
|
|
86
|
+
})}
|
|
87
|
+
/>,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(screen.getByText('Searching product documentation')).toBeInTheDocument();
|
|
91
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -103,6 +103,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
103
103
|
renderContextItem,
|
|
104
104
|
renderMention,
|
|
105
105
|
renderEntityCard,
|
|
106
|
+
refs,
|
|
106
107
|
onAskSelect,
|
|
107
108
|
NavLinkAnchor,
|
|
108
109
|
...props
|
|
@@ -127,6 +128,11 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
127
128
|
// literal `[card://...]` URL.
|
|
128
129
|
const hasMarkerSupport = !!renderEntityCard;
|
|
129
130
|
|
|
131
|
+
const refsByKey = useMemo(
|
|
132
|
+
() => new Map((refs ?? []).map(reference => [`${reference.type}:${reference.id}`, reference])),
|
|
133
|
+
[refs],
|
|
134
|
+
);
|
|
135
|
+
|
|
130
136
|
const segments = useMemo(() => normalizeContent(content), [content]);
|
|
131
137
|
|
|
132
138
|
const askRuns = useMemo(() => groupAskRuns(segments), [segments]);
|
|
@@ -180,7 +186,14 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
180
186
|
// (and its open menu) survives across chunks. Invalidated per key when the
|
|
181
187
|
// render fn identity changes.
|
|
182
188
|
const renderedCardNodeCache = useRef(
|
|
183
|
-
new Map<
|
|
189
|
+
new Map<
|
|
190
|
+
string,
|
|
191
|
+
{
|
|
192
|
+
render: ((ref: ChatRef) => React.ReactNode) | undefined;
|
|
193
|
+
reference: ChatRef;
|
|
194
|
+
node: React.ReactNode;
|
|
195
|
+
}
|
|
196
|
+
>(),
|
|
184
197
|
);
|
|
185
198
|
|
|
186
199
|
/**
|
|
@@ -242,10 +255,17 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
242
255
|
// menu / re-fetching) on every stream chunk. Also dedups the same
|
|
243
256
|
// key emitted twice within one message.
|
|
244
257
|
let entry = cache.get(key);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
258
|
+
const enrichedRef = refsByKey.get(key);
|
|
259
|
+
const refForRender: ChatRef = enrichedRef ?? {
|
|
260
|
+
type: cardType,
|
|
261
|
+
id: cardId,
|
|
262
|
+
title: cardId,
|
|
263
|
+
url: null,
|
|
264
|
+
};
|
|
265
|
+
if (!entry || entry.render !== render || entry.reference !== refForRender) {
|
|
266
|
+
// Prefer same-turn SOURCES metadata when available. Otherwise
|
|
267
|
+
// cards hydrate by id from the host's per-object APIs, so a
|
|
268
|
+
// minimal {type, id} descriptor still mounts the loader.
|
|
249
269
|
// For types with nothing to fetch the host's render() returns
|
|
250
270
|
// null and we fall through to the bare-cardId fallback in the
|
|
251
271
|
// `<a card://…>` override below.
|
|
@@ -257,13 +277,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
257
277
|
// the id rather than `undefined`) and `url` to null (matches
|
|
258
278
|
// the no-link semantics fetch-mode cards rely on — they
|
|
259
279
|
// resolve their own URL after fetch).
|
|
260
|
-
|
|
261
|
-
type: cardType,
|
|
262
|
-
id: cardId,
|
|
263
|
-
title: cardId,
|
|
264
|
-
url: null,
|
|
265
|
-
};
|
|
266
|
-
entry = { render, node: render(refForRender) };
|
|
280
|
+
entry = { render, reference: refForRender, node: render(refForRender) };
|
|
267
281
|
cache.set(key, entry);
|
|
268
282
|
}
|
|
269
283
|
const rendered = entry.node;
|
|
@@ -326,7 +340,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
326
340
|
}
|
|
327
341
|
});
|
|
328
342
|
return { inlineByKey, partsBySegment, usedKeys };
|
|
329
|
-
}, [hasMarkerSupport, renderEntityCard, segments]);
|
|
343
|
+
}, [hasMarkerSupport, refsByKey, renderEntityCard, segments]);
|
|
330
344
|
|
|
331
345
|
// Drop cached nodes for markers no longer present so the cache can't grow
|
|
332
346
|
// unbounded as a long message's markers change. Deliberately an EFFECT,
|
|
@@ -716,6 +730,7 @@ const MemoizedChatMessageEnhanced = memo(ChatMessageEnhanced, (prevProps, nextPr
|
|
|
716
730
|
// equality holds across streaming chunks.
|
|
717
731
|
prevProps.renderMention === nextProps.renderMention &&
|
|
718
732
|
prevProps.renderEntityCard === nextProps.renderEntityCard &&
|
|
733
|
+
prevProps.refs === nextProps.refs &&
|
|
719
734
|
// Same stability contract as the renderers above: hosts pass a `useCallback`
|
|
720
735
|
// (EmbeddableChat passes its memoized `handleSend`), so this holds across
|
|
721
736
|
// streaming chunks instead of re-rendering every ask card per chunk.
|