@ahmedramez-org/nexus-chat 1.0.0-beta.2 → 1.0.0-beta.21
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/README.md +134 -44
- package/contract/conformance.js +30 -5
- package/contract/index.d.ts +96 -8
- package/contract/index.js +14 -0
- package/index.d.ts +23 -3
- package/index.js +238 -14
- package/package.json +19 -4
- package/react.d.ts +77 -17
- package/react.js +241 -35
- package/window.js +387 -0
- package/remote/assets/AgentOptionControl-BnqfB5W3.js +0 -1
- package/remote/assets/CsvPreview-DD2VELWa.js +0 -10
- package/remote/assets/ErrorCard-Bu_0QAJ9.js +0 -1
- package/remote/assets/HighlightedCode-DpxGYHfF.js +0 -1
- package/remote/assets/HistorySearchDialog-q4cvx5jy.js +0 -41
- package/remote/assets/HostBridgeProvider-BV-UGU3Q.js +0 -1
- package/remote/assets/HtmlArtifactRenderer-EO_cwaEm.js +0 -1
- package/remote/assets/I18nContext-BlQKoDmv.js +0 -24
- package/remote/assets/PreviewState-VcIZyCXJ.js +0 -1
- package/remote/assets/SourceCitations-DvApeC9N.js +0 -1
- package/remote/assets/SourcesArtifactRenderer-BqnIOctm.js +0 -1
- package/remote/assets/TableArtifactRenderer-BLZLQMxU.js +0 -2
- package/remote/assets/TiptapComposerInput-CmxEiBlE.js +0 -132
- package/remote/assets/UnsupportedArtifact-CeOCu_J6.js +0 -1
- package/remote/assets/_virtual_mf-localSharedImportMap___mfe_internal__nexusChat__mf_owner__1-CObPUVU6.js +0 -1
- package/remote/assets/ar-BV81JAir.js +0 -1
- package/remote/assets/button-CO0tH6F2.js +0 -1
- package/remote/assets/client-Bv7EbCrM.js +0 -2
- package/remote/assets/composite-giRhJC3d.js +0 -1
- package/remote/assets/conformance-entry-uotihsU2.js +0 -2
- package/remote/assets/createLucideIcon-D1A0JYKN.js +0 -1
- package/remote/assets/de-BXJlhmC2.js +0 -1
- package/remote/assets/defineProperty-BbfpZ9Tg.js +0 -1
- package/remote/assets/dist-MaNcI-Om.js +0 -4
- package/remote/assets/errorReporter-hCgThzof.js +0 -1
- package/remote/assets/es-nnw68YFM.js +0 -1
- package/remote/assets/federation-entry-B8pojL73.css +0 -2
- package/remote/assets/federation-entry-BA9UpYiM.js +0 -65
- package/remote/assets/floating-ui.dom-B99QSfPH.js +0 -1
- package/remote/assets/floating-ui.utils.dom-ObnfY4F6.js +0 -1
- package/remote/assets/fr-CJIAbbzn.js +0 -1
- package/remote/assets/highlighter-C37NKSjx.js +0 -19
- package/remote/assets/hostInit-YMtJ2r-g.js +0 -1
- package/remote/assets/it-D3TiEKfy.js +0 -1
- package/remote/assets/ja-ZB0L4Lsc.js +0 -1
- package/remote/assets/jsx-runtime-DE3RlOCf.js +0 -1
- package/remote/assets/lib-CjT9YRPC.js +0 -1
- package/remote/assets/modelProviderIcons-fiyx4SaW.js +0 -1
- package/remote/assets/react-dom-FxOPXJ08.js +0 -1
- package/remote/assets/rehype-raw-CNOzW4vu.js +0 -2
- package/remote/assets/roboto-latin-400-italic-CSuqwwKr.woff2 +0 -0
- package/remote/assets/roboto-latin-400-normal-BqEyEoaF.woff2 +0 -0
- package/remote/assets/roboto-latin-500-italic-DOe3GFcv.woff2 +0 -0
- package/remote/assets/roboto-latin-500-normal-7RbcRiD8.woff2 +0 -0
- package/remote/assets/roboto-latin-700-italic-Cb9agdGy.woff2 +0 -0
- package/remote/assets/roboto-latin-700-normal-BZpUvMxY.woff2 +0 -0
- package/remote/assets/roboto-latin-ext-400-italic-BilqUbDw.woff2 +0 -0
- package/remote/assets/roboto-latin-ext-400-normal-C3tdtHj3.woff2 +0 -0
- package/remote/assets/roboto-latin-ext-500-italic-B5teBpxj.woff2 +0 -0
- package/remote/assets/roboto-latin-ext-500-normal-pMCM9Ixg.woff2 +0 -0
- package/remote/assets/roboto-latin-ext-700-italic-CBdFfgzf.woff2 +0 -0
- package/remote/assets/roboto-latin-ext-700-normal-DSBUz0N1.woff2 +0 -0
- package/remote/assets/rolldown-runtime-hePW80VL.js +0 -1
- package/remote/assets/sessionTitle-C9LhAN0r.js +0 -1
- package/remote/assets/spinner-eqRx6Fq-.js +0 -1
- package/remote/assets/sqlFormatting-BjJIfUN_.js +0 -7
- package/remote/assets/src-CIHHGjAO.js +0 -1
- package/remote/assets/store-FaSYSP4_.js +0 -1
- package/remote/assets/useBaseUiId-Dpqq0brK.js +0 -1
- package/remote/assets/usePositioner-BhCjFmeW.js +0 -1
- package/remote/assets/virtualExposes-C0oWo8z2.js +0 -1
- package/remote/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__nexusChat__remoteEntry_js-5uUvXWWH.js +0 -1
- package/remote/assets/virtual_mf-exposes-ssr___mfe_internal__nexusChat__remoteEntry_js-WBZ0Muv9.js +0 -2
- package/remote/assets/virtual_mf-exposes___mfe_internal__nexusChat__remoteEntry_js-BGZ2nkwm.js +0 -2
- package/remote/assets/vite-preload-helper-B7qeedMF.js +0 -1
- package/remote/assets/zh-De2Ilp0M.js +0 -1
- package/remote/remoteEntry.js +0 -1
- package/remote/remoteEntry.ssr.js +0 -1
package/react.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
// <NexusChat /> — the connector as a React component
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// app
|
|
6
|
-
|
|
1
|
+
// <NexusChat /> — the connector as a React component (the app's React 18 or
|
|
2
|
+
// 19 renders only this wrapper; the chat runs its own React). Two ways in:
|
|
3
|
+
//
|
|
4
|
+
// - Direct: the chat mounts into an element this renders, sharing the page's
|
|
5
|
+
// session. It fills its box and the app draws any frame around it, or, with
|
|
6
|
+
// `modes` or `defaultMode` instead of a `window`, it draws the same window
|
|
7
|
+
// as embedded.
|
|
8
|
+
// - Embedded (`embedUrl`): the chat runs in an <iframe> served by Incorta, on
|
|
9
|
+
// the user's own Incorta session, so the app never holds a token. This
|
|
10
|
+
// draws the window too: a launcher icon that opens the chat floating,
|
|
11
|
+
// docked or fullscreen (the app picks which with `modes`).
|
|
12
|
+
import { createElement as h, useEffect, useRef, useState } from 'react';
|
|
7
13
|
|
|
8
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
CONTRACT_VERSION,
|
|
16
|
+
forgetNexusChat,
|
|
17
|
+
loadNexusChat,
|
|
18
|
+
showNexusChatSkeleton,
|
|
19
|
+
} from './index.js';
|
|
20
|
+
import { connectEmbeddedChat, createChatWindow, embedSrc, initialView } from './window.js';
|
|
9
21
|
|
|
10
22
|
function emitter() {
|
|
11
23
|
const listeners = new Set();
|
|
@@ -18,7 +30,7 @@ function emitter() {
|
|
|
18
30
|
};
|
|
19
31
|
}
|
|
20
32
|
|
|
21
|
-
|
|
33
|
+
function DirectChat({
|
|
22
34
|
theme,
|
|
23
35
|
locale,
|
|
24
36
|
direction,
|
|
@@ -39,12 +51,24 @@ export function NexusChat({
|
|
|
39
51
|
const windowState = useRef(null);
|
|
40
52
|
windowState.current ??= emitter();
|
|
41
53
|
const hasWindow = Boolean(hostWindow);
|
|
54
|
+
// Until the chat is mounted the skeleton holds the box, the same loading
|
|
55
|
+
// face as the embedded window, so every app shows one.
|
|
56
|
+
const [status, setStatus] = useState('loading');
|
|
42
57
|
|
|
43
58
|
useEffect(
|
|
44
59
|
() => environment.current.notify(),
|
|
45
60
|
[theme, locale, direction, tenantId, user?.id, user?.name],
|
|
46
61
|
);
|
|
47
|
-
useEffect(
|
|
62
|
+
useEffect(
|
|
63
|
+
() => windowState.current.notify(),
|
|
64
|
+
[
|
|
65
|
+
hostWindow?.docked,
|
|
66
|
+
hostWindow?.canDock,
|
|
67
|
+
hostWindow?.mode,
|
|
68
|
+
hostWindow?.modes?.join(),
|
|
69
|
+
hostWindow?.hidden,
|
|
70
|
+
],
|
|
71
|
+
);
|
|
48
72
|
|
|
49
73
|
// Mounts once per element (and again only if `window` appears or goes):
|
|
50
74
|
// options other than the ones above are read at mount. It loads first and
|
|
@@ -55,10 +79,26 @@ export function NexusChat({
|
|
|
55
79
|
let cancelled = false;
|
|
56
80
|
const current = () => latest.current;
|
|
57
81
|
const { assetsUrl, timeoutMs, ...options } = mountOptions;
|
|
82
|
+
const source = { copilotUrl: options.connection?.copilotUrl, assetsUrl, timeoutMs };
|
|
83
|
+
const { onError, onFatal } = options.events ?? {};
|
|
84
|
+
// A contract `environment` or `window` the app built itself is used as is.
|
|
85
|
+
const ownWindow = typeof hostWindow?.get === 'function';
|
|
58
86
|
const mountOptionsFor = () => ({
|
|
59
87
|
version: CONTRACT_VERSION,
|
|
60
88
|
...options,
|
|
61
|
-
|
|
89
|
+
// A chat that failed while running is fetched afresh by the app's retry.
|
|
90
|
+
...(onFatal
|
|
91
|
+
? {
|
|
92
|
+
events: {
|
|
93
|
+
...options.events,
|
|
94
|
+
onFatal: (error) => {
|
|
95
|
+
forgetNexusChat(source);
|
|
96
|
+
onFatal(error);
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
: {}),
|
|
101
|
+
environment: options.environment ?? {
|
|
62
102
|
get: () => {
|
|
63
103
|
const props = current();
|
|
64
104
|
return {
|
|
@@ -71,44 +111,210 @@ export function NexusChat({
|
|
|
71
111
|
},
|
|
72
112
|
subscribe: environment.current.subscribe,
|
|
73
113
|
},
|
|
74
|
-
...(
|
|
75
|
-
? {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
114
|
+
...(ownWindow
|
|
115
|
+
? { window: hostWindow }
|
|
116
|
+
: hasWindow
|
|
117
|
+
? {
|
|
118
|
+
window: {
|
|
119
|
+
get: () => {
|
|
120
|
+
const {
|
|
121
|
+
docked = false,
|
|
122
|
+
canDock = false,
|
|
123
|
+
mode,
|
|
124
|
+
modes,
|
|
125
|
+
hidden,
|
|
126
|
+
} = current().hostWindow ?? {};
|
|
127
|
+
return {
|
|
128
|
+
docked,
|
|
129
|
+
canDock,
|
|
130
|
+
...(mode && modes ? { mode, modes } : {}),
|
|
131
|
+
...(hidden ? { hidden: true } : {}),
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
subscribe: windowState.current.subscribe,
|
|
135
|
+
setDocked: (docked) => current().hostWindow?.setDocked(docked),
|
|
136
|
+
// Read at mount, like the rest of `window`'s shape.
|
|
137
|
+
...(hostWindow.setMode
|
|
138
|
+
? { setMode: (mode) => current().hostWindow?.setMode?.(mode) }
|
|
139
|
+
: {}),
|
|
140
|
+
close: () => current().hostWindow?.close(),
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
: {}),
|
|
87
144
|
});
|
|
88
145
|
const fail = (error) => {
|
|
89
|
-
if (
|
|
146
|
+
if (cancelled) return;
|
|
147
|
+
// The app draws its own failure (and Retry) where the chat would be.
|
|
148
|
+
setStatus('failed');
|
|
149
|
+
current().onLoadError?.(error);
|
|
90
150
|
};
|
|
91
|
-
|
|
92
|
-
|
|
151
|
+
// In a promise, so a missing source reaches onLoadError like any load failure.
|
|
152
|
+
Promise.resolve()
|
|
153
|
+
.then(() => loadNexusChat(source))
|
|
154
|
+
.then((mount) => {
|
|
93
155
|
if (cancelled) return;
|
|
94
156
|
try {
|
|
95
157
|
handle = mount(element.current, mountOptionsFor());
|
|
96
158
|
} catch (error) {
|
|
159
|
+
forgetNexusChat(source);
|
|
97
160
|
return fail(error);
|
|
98
161
|
}
|
|
162
|
+
setStatus('ready');
|
|
99
163
|
current().onReady?.(handle);
|
|
100
|
-
},
|
|
101
|
-
fail,
|
|
102
|
-
);
|
|
164
|
+
}, fail);
|
|
103
165
|
return () => {
|
|
104
166
|
cancelled = true;
|
|
105
|
-
handle
|
|
167
|
+
const mounted = handle;
|
|
168
|
+
handle = undefined;
|
|
169
|
+
if (!mounted) return;
|
|
170
|
+
try {
|
|
171
|
+
mounted.unmount();
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (onError) onError(error, { scope: 'unmount' });
|
|
174
|
+
else console.error('NexusChat could not be unmounted cleanly.', error);
|
|
175
|
+
}
|
|
176
|
+
current().onReady?.(null);
|
|
106
177
|
};
|
|
107
178
|
}, [hasWindow]);
|
|
108
179
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
style: { width: '100%', height: '100%'
|
|
113
|
-
|
|
180
|
+
// The app's className and style stay on the chat's own root (the mount element).
|
|
181
|
+
return h(
|
|
182
|
+
'div',
|
|
183
|
+
{ style: { position: 'relative', width: '100%', height: '100%' } },
|
|
184
|
+
h('div', { ref: element, className, style: { width: '100%', height: '100%', ...style } }),
|
|
185
|
+
// A failure leaves at once: the app's own failure takes the box.
|
|
186
|
+
h(Skeleton, { status, theme: theme ?? mountOptions.environment?.get().theme }),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The skeleton (index.js) over the box that holds this, until the chat is
|
|
192
|
+
* 'ready' (it fades) or 'failed' (gone at once).
|
|
193
|
+
*/
|
|
194
|
+
function Skeleton({ status, theme }) {
|
|
195
|
+
const anchor = useRef(null);
|
|
196
|
+
const skeleton = useRef(null);
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
const shown = showNexusChatSkeleton(anchor.current.parentNode, theme);
|
|
199
|
+
skeleton.current = shown;
|
|
200
|
+
return shown.remove;
|
|
201
|
+
}, []);
|
|
202
|
+
useEffect(() => skeleton.current.setTheme(theme), [theme]);
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
if (status === 'ready') skeleton.current.done();
|
|
205
|
+
if (status === 'failed') skeleton.current.remove();
|
|
206
|
+
}, [status]);
|
|
207
|
+
return h('span', { ref: anchor, hidden: true });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---- the window: window.js draws it; React renders the panel's content
|
|
211
|
+
|
|
212
|
+
const NAME = 'Incorta Intelligence';
|
|
213
|
+
/** The panel React renders; window.js places it (React never styles it again). */
|
|
214
|
+
const PANEL = { display: 'none' };
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The window (window.js) around a panel this renders, re-rendering when it
|
|
218
|
+
* changes. `host` holds the panel and the launcher; until the window exists
|
|
219
|
+
* (the first effect) the panel is opened as `defaultMode` says, so a page
|
|
220
|
+
* rendered on a server already holds its chat.
|
|
221
|
+
*/
|
|
222
|
+
function useChatWindow({ modes, defaultMode, theme }) {
|
|
223
|
+
const host = useRef(null);
|
|
224
|
+
const panel = useRef(null);
|
|
225
|
+
const [win, setWin] = useState(null);
|
|
226
|
+
const [, changed] = useState(0);
|
|
227
|
+
const [openedAtFirst] = useState(() => initialView(modes, defaultMode) !== 'minimized');
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
const created = createChatWindow({
|
|
230
|
+
into: host.current,
|
|
231
|
+
panel: panel.current,
|
|
232
|
+
modes,
|
|
233
|
+
defaultMode,
|
|
234
|
+
theme,
|
|
235
|
+
});
|
|
236
|
+
const unsubscribe = created.subscribe(() => changed((count) => count + 1));
|
|
237
|
+
setWin(created);
|
|
238
|
+
return () => {
|
|
239
|
+
unsubscribe();
|
|
240
|
+
created.destroy();
|
|
241
|
+
};
|
|
242
|
+
}, []);
|
|
243
|
+
useEffect(() => win?.setTheme(theme), [win, theme]);
|
|
244
|
+
return { host, panel, win, opened: win ? win.opened : openedAtFirst };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---- direct, in the window: the app gives `modes` or `defaultMode` instead of a `window`
|
|
248
|
+
|
|
249
|
+
function WindowedChat({ modes, defaultMode, onReady, ...props }) {
|
|
250
|
+
const theme = props.theme ?? props.environment?.get().theme;
|
|
251
|
+
const { host, panel, win, opened } = useChatWindow({ modes, defaultMode, theme });
|
|
252
|
+
return h(
|
|
253
|
+
'div',
|
|
254
|
+
{ ref: host },
|
|
255
|
+
h(
|
|
256
|
+
'div',
|
|
257
|
+
{ ref: panel, style: PANEL },
|
|
258
|
+
opened &&
|
|
259
|
+
win &&
|
|
260
|
+
h(DirectChat, {
|
|
261
|
+
...props,
|
|
262
|
+
window: {
|
|
263
|
+
...win.state(),
|
|
264
|
+
setMode: win.setMode,
|
|
265
|
+
setDocked: win.setDocked,
|
|
266
|
+
close: win.close,
|
|
267
|
+
},
|
|
268
|
+
onReady: (chat) => {
|
|
269
|
+
win.setMounted(Boolean(chat));
|
|
270
|
+
onReady?.(chat);
|
|
271
|
+
},
|
|
272
|
+
}),
|
|
273
|
+
),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---- embedded: the chat in an Incorta-served iframe, in the window
|
|
278
|
+
|
|
279
|
+
function EmbeddedChat({ embedUrl, modes, defaultMode, theme = 'light', locale }) {
|
|
280
|
+
const { host, panel, win, opened } = useChatWindow({ modes, defaultMode, theme });
|
|
281
|
+
const frame = useRef(null);
|
|
282
|
+
const embedded = useRef(null);
|
|
283
|
+
// Set once: a new src would reload the chat. Later theme changes go by message.
|
|
284
|
+
const [src] = useState(() => embedSrc(embedUrl, theme, locale));
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
if (!win || !opened) return undefined;
|
|
287
|
+
const connected = connectEmbeddedChat(win, frame.current, { embedUrl, theme, locale });
|
|
288
|
+
embedded.current = connected;
|
|
289
|
+
return () => {
|
|
290
|
+
connected.destroy();
|
|
291
|
+
embedded.current = null;
|
|
292
|
+
};
|
|
293
|
+
}, [win, opened, embedUrl]);
|
|
294
|
+
useEffect(() => embedded.current?.setTheme(theme), [theme]);
|
|
295
|
+
useEffect(() => embedded.current?.setLocale(locale), [locale]);
|
|
296
|
+
|
|
297
|
+
return h(
|
|
298
|
+
'div',
|
|
299
|
+
{ ref: host },
|
|
300
|
+
h(
|
|
301
|
+
'div',
|
|
302
|
+
{ ref: panel, style: PANEL },
|
|
303
|
+
opened &&
|
|
304
|
+
h('iframe', {
|
|
305
|
+
ref: frame,
|
|
306
|
+
title: NAME,
|
|
307
|
+
src,
|
|
308
|
+
allow: 'storage-access; microphone; clipboard-write',
|
|
309
|
+
style: { display: 'block', width: '100%', height: '100%', border: 0 },
|
|
310
|
+
}),
|
|
311
|
+
),
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function NexusChat(props) {
|
|
316
|
+
if (props.embedUrl) return h(EmbeddedChat, props);
|
|
317
|
+
// Direct: the app's own `window` (or none) keeps the chat in its box.
|
|
318
|
+
const windowed = !props.window && (props.modes !== undefined || props.defaultMode !== undefined);
|
|
319
|
+
return h(windowed ? WindowedChat : DirectChat, props);
|
|
114
320
|
}
|