@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.
Files changed (78) hide show
  1. package/README.md +134 -44
  2. package/contract/conformance.js +30 -5
  3. package/contract/index.d.ts +96 -8
  4. package/contract/index.js +14 -0
  5. package/index.d.ts +23 -3
  6. package/index.js +238 -14
  7. package/package.json +19 -4
  8. package/react.d.ts +77 -17
  9. package/react.js +241 -35
  10. package/window.js +387 -0
  11. package/remote/assets/AgentOptionControl-BnqfB5W3.js +0 -1
  12. package/remote/assets/CsvPreview-DD2VELWa.js +0 -10
  13. package/remote/assets/ErrorCard-Bu_0QAJ9.js +0 -1
  14. package/remote/assets/HighlightedCode-DpxGYHfF.js +0 -1
  15. package/remote/assets/HistorySearchDialog-q4cvx5jy.js +0 -41
  16. package/remote/assets/HostBridgeProvider-BV-UGU3Q.js +0 -1
  17. package/remote/assets/HtmlArtifactRenderer-EO_cwaEm.js +0 -1
  18. package/remote/assets/I18nContext-BlQKoDmv.js +0 -24
  19. package/remote/assets/PreviewState-VcIZyCXJ.js +0 -1
  20. package/remote/assets/SourceCitations-DvApeC9N.js +0 -1
  21. package/remote/assets/SourcesArtifactRenderer-BqnIOctm.js +0 -1
  22. package/remote/assets/TableArtifactRenderer-BLZLQMxU.js +0 -2
  23. package/remote/assets/TiptapComposerInput-CmxEiBlE.js +0 -132
  24. package/remote/assets/UnsupportedArtifact-CeOCu_J6.js +0 -1
  25. package/remote/assets/_virtual_mf-localSharedImportMap___mfe_internal__nexusChat__mf_owner__1-CObPUVU6.js +0 -1
  26. package/remote/assets/ar-BV81JAir.js +0 -1
  27. package/remote/assets/button-CO0tH6F2.js +0 -1
  28. package/remote/assets/client-Bv7EbCrM.js +0 -2
  29. package/remote/assets/composite-giRhJC3d.js +0 -1
  30. package/remote/assets/conformance-entry-uotihsU2.js +0 -2
  31. package/remote/assets/createLucideIcon-D1A0JYKN.js +0 -1
  32. package/remote/assets/de-BXJlhmC2.js +0 -1
  33. package/remote/assets/defineProperty-BbfpZ9Tg.js +0 -1
  34. package/remote/assets/dist-MaNcI-Om.js +0 -4
  35. package/remote/assets/errorReporter-hCgThzof.js +0 -1
  36. package/remote/assets/es-nnw68YFM.js +0 -1
  37. package/remote/assets/federation-entry-B8pojL73.css +0 -2
  38. package/remote/assets/federation-entry-BA9UpYiM.js +0 -65
  39. package/remote/assets/floating-ui.dom-B99QSfPH.js +0 -1
  40. package/remote/assets/floating-ui.utils.dom-ObnfY4F6.js +0 -1
  41. package/remote/assets/fr-CJIAbbzn.js +0 -1
  42. package/remote/assets/highlighter-C37NKSjx.js +0 -19
  43. package/remote/assets/hostInit-YMtJ2r-g.js +0 -1
  44. package/remote/assets/it-D3TiEKfy.js +0 -1
  45. package/remote/assets/ja-ZB0L4Lsc.js +0 -1
  46. package/remote/assets/jsx-runtime-DE3RlOCf.js +0 -1
  47. package/remote/assets/lib-CjT9YRPC.js +0 -1
  48. package/remote/assets/modelProviderIcons-fiyx4SaW.js +0 -1
  49. package/remote/assets/react-dom-FxOPXJ08.js +0 -1
  50. package/remote/assets/rehype-raw-CNOzW4vu.js +0 -2
  51. package/remote/assets/roboto-latin-400-italic-CSuqwwKr.woff2 +0 -0
  52. package/remote/assets/roboto-latin-400-normal-BqEyEoaF.woff2 +0 -0
  53. package/remote/assets/roboto-latin-500-italic-DOe3GFcv.woff2 +0 -0
  54. package/remote/assets/roboto-latin-500-normal-7RbcRiD8.woff2 +0 -0
  55. package/remote/assets/roboto-latin-700-italic-Cb9agdGy.woff2 +0 -0
  56. package/remote/assets/roboto-latin-700-normal-BZpUvMxY.woff2 +0 -0
  57. package/remote/assets/roboto-latin-ext-400-italic-BilqUbDw.woff2 +0 -0
  58. package/remote/assets/roboto-latin-ext-400-normal-C3tdtHj3.woff2 +0 -0
  59. package/remote/assets/roboto-latin-ext-500-italic-B5teBpxj.woff2 +0 -0
  60. package/remote/assets/roboto-latin-ext-500-normal-pMCM9Ixg.woff2 +0 -0
  61. package/remote/assets/roboto-latin-ext-700-italic-CBdFfgzf.woff2 +0 -0
  62. package/remote/assets/roboto-latin-ext-700-normal-DSBUz0N1.woff2 +0 -0
  63. package/remote/assets/rolldown-runtime-hePW80VL.js +0 -1
  64. package/remote/assets/sessionTitle-C9LhAN0r.js +0 -1
  65. package/remote/assets/spinner-eqRx6Fq-.js +0 -1
  66. package/remote/assets/sqlFormatting-BjJIfUN_.js +0 -7
  67. package/remote/assets/src-CIHHGjAO.js +0 -1
  68. package/remote/assets/store-FaSYSP4_.js +0 -1
  69. package/remote/assets/useBaseUiId-Dpqq0brK.js +0 -1
  70. package/remote/assets/usePositioner-BhCjFmeW.js +0 -1
  71. package/remote/assets/virtualExposes-C0oWo8z2.js +0 -1
  72. package/remote/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__nexusChat__remoteEntry_js-5uUvXWWH.js +0 -1
  73. package/remote/assets/virtual_mf-exposes-ssr___mfe_internal__nexusChat__remoteEntry_js-WBZ0Muv9.js +0 -2
  74. package/remote/assets/virtual_mf-exposes___mfe_internal__nexusChat__remoteEntry_js-BGZ2nkwm.js +0 -2
  75. package/remote/assets/vite-preload-helper-B7qeedMF.js +0 -1
  76. package/remote/assets/zh-De2Ilp0M.js +0 -1
  77. package/remote/remoteEntry.js +0 -1
  78. package/remote/remoteEntry.ssr.js +0 -1
package/react.js CHANGED
@@ -1,11 +1,23 @@
1
- // <NexusChat /> — the connector as a React component. It renders one element
2
- // the chat fills, mounts the chat into it once, and keeps the chat told about
3
- // the props that change (theme, locale, direction, tenant, user, window
4
- // state). The chat itself still runs its own React; this file only uses the
5
- // app's React (18 or 19) for the wrapper.
6
- import { createElement, useEffect, useRef } from 'react';
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 { CONTRACT_VERSION, loadNexusChat } from './index.js';
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
- export function NexusChat({
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(() => windowState.current.notify(), [hostWindow?.docked, hostWindow?.canDock]);
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
- environment: {
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
- ...(hasWindow
75
- ? {
76
- window: {
77
- get: () => ({
78
- docked: current().hostWindow?.docked ?? false,
79
- canDock: current().hostWindow?.canDock ?? false,
80
- }),
81
- subscribe: windowState.current.subscribe,
82
- setDocked: (docked) => current().hostWindow?.setDocked(docked),
83
- close: () => current().hostWindow?.close(),
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 (!cancelled) current().onLoadError?.(error);
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
- loadNexusChat({ copilotUrl: options.connection?.copilotUrl, assetsUrl, timeoutMs }).then(
92
- (mount) => {
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?.unmount();
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
- return createElement('div', {
110
- ref: element,
111
- className,
112
- style: { width: '100%', height: '100%', ...style },
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
  }