@ahmedramez-org/nexus-chat 1.0.0-beta.1 → 1.0.0-beta.3

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 (77) hide show
  1. package/README.md +65 -40
  2. package/contract/conformance.js +6 -4
  3. package/contract/index.d.ts +39 -2
  4. package/contract/index.js +12 -0
  5. package/index.d.ts +8 -2
  6. package/index.js +84 -3
  7. package/package.json +2 -4
  8. package/react.d.ts +50 -14
  9. package/react.js +367 -37
  10. package/remote/assets/AgentOptionControl-BnqfB5W3.js +0 -1
  11. package/remote/assets/CsvPreview-DD2VELWa.js +0 -10
  12. package/remote/assets/ErrorCard-Bu_0QAJ9.js +0 -1
  13. package/remote/assets/HighlightedCode-DpxGYHfF.js +0 -1
  14. package/remote/assets/HistorySearchDialog-q4cvx5jy.js +0 -41
  15. package/remote/assets/HostBridgeProvider-BV-UGU3Q.js +0 -1
  16. package/remote/assets/HtmlArtifactRenderer-EO_cwaEm.js +0 -1
  17. package/remote/assets/I18nContext-BlQKoDmv.js +0 -24
  18. package/remote/assets/PreviewState-VcIZyCXJ.js +0 -1
  19. package/remote/assets/SourceCitations-DvApeC9N.js +0 -1
  20. package/remote/assets/SourcesArtifactRenderer-BqnIOctm.js +0 -1
  21. package/remote/assets/TableArtifactRenderer-BLZLQMxU.js +0 -2
  22. package/remote/assets/TiptapComposerInput-CmxEiBlE.js +0 -132
  23. package/remote/assets/UnsupportedArtifact-CeOCu_J6.js +0 -1
  24. package/remote/assets/_virtual_mf-localSharedImportMap___mfe_internal__nexusChat__mf_owner__1-CObPUVU6.js +0 -1
  25. package/remote/assets/ar-BV81JAir.js +0 -1
  26. package/remote/assets/button-CO0tH6F2.js +0 -1
  27. package/remote/assets/client-Bv7EbCrM.js +0 -2
  28. package/remote/assets/composite-giRhJC3d.js +0 -1
  29. package/remote/assets/conformance-entry-uotihsU2.js +0 -2
  30. package/remote/assets/createLucideIcon-D1A0JYKN.js +0 -1
  31. package/remote/assets/de-BXJlhmC2.js +0 -1
  32. package/remote/assets/defineProperty-BbfpZ9Tg.js +0 -1
  33. package/remote/assets/dist-MaNcI-Om.js +0 -4
  34. package/remote/assets/errorReporter-hCgThzof.js +0 -1
  35. package/remote/assets/es-nnw68YFM.js +0 -1
  36. package/remote/assets/federation-entry-B8pojL73.css +0 -2
  37. package/remote/assets/federation-entry-DL02k5Y5.js +0 -65
  38. package/remote/assets/floating-ui.dom-B99QSfPH.js +0 -1
  39. package/remote/assets/floating-ui.utils.dom-ObnfY4F6.js +0 -1
  40. package/remote/assets/fr-CJIAbbzn.js +0 -1
  41. package/remote/assets/highlighter-C37NKSjx.js +0 -19
  42. package/remote/assets/hostInit-YMtJ2r-g.js +0 -1
  43. package/remote/assets/it-D3TiEKfy.js +0 -1
  44. package/remote/assets/ja-ZB0L4Lsc.js +0 -1
  45. package/remote/assets/jsx-runtime-DE3RlOCf.js +0 -1
  46. package/remote/assets/lib-CjT9YRPC.js +0 -1
  47. package/remote/assets/modelProviderIcons-fiyx4SaW.js +0 -1
  48. package/remote/assets/react-dom-FxOPXJ08.js +0 -1
  49. package/remote/assets/rehype-raw-CNOzW4vu.js +0 -2
  50. package/remote/assets/roboto-latin-400-italic-CSuqwwKr.woff2 +0 -0
  51. package/remote/assets/roboto-latin-400-normal-BqEyEoaF.woff2 +0 -0
  52. package/remote/assets/roboto-latin-500-italic-DOe3GFcv.woff2 +0 -0
  53. package/remote/assets/roboto-latin-500-normal-7RbcRiD8.woff2 +0 -0
  54. package/remote/assets/roboto-latin-700-italic-Cb9agdGy.woff2 +0 -0
  55. package/remote/assets/roboto-latin-700-normal-BZpUvMxY.woff2 +0 -0
  56. package/remote/assets/roboto-latin-ext-400-italic-BilqUbDw.woff2 +0 -0
  57. package/remote/assets/roboto-latin-ext-400-normal-C3tdtHj3.woff2 +0 -0
  58. package/remote/assets/roboto-latin-ext-500-italic-B5teBpxj.woff2 +0 -0
  59. package/remote/assets/roboto-latin-ext-500-normal-pMCM9Ixg.woff2 +0 -0
  60. package/remote/assets/roboto-latin-ext-700-italic-CBdFfgzf.woff2 +0 -0
  61. package/remote/assets/roboto-latin-ext-700-normal-DSBUz0N1.woff2 +0 -0
  62. package/remote/assets/rolldown-runtime-hePW80VL.js +0 -1
  63. package/remote/assets/sessionTitle-C9LhAN0r.js +0 -1
  64. package/remote/assets/spinner-eqRx6Fq-.js +0 -1
  65. package/remote/assets/sqlFormatting-BjJIfUN_.js +0 -7
  66. package/remote/assets/src-CIHHGjAO.js +0 -1
  67. package/remote/assets/store-FaSYSP4_.js +0 -1
  68. package/remote/assets/useBaseUiId-Dpqq0brK.js +0 -1
  69. package/remote/assets/usePositioner-BhCjFmeW.js +0 -1
  70. package/remote/assets/virtualExposes-DgWzju8R.js +0 -1
  71. package/remote/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__nexusChat__remoteEntry_js-BCqsZDEx.js +0 -1
  72. package/remote/assets/virtual_mf-exposes-ssr___mfe_internal__nexusChat__remoteEntry_js-nszk7xTq.js +0 -2
  73. package/remote/assets/virtual_mf-exposes___mfe_internal__nexusChat__remoteEntry_js-BkoNn4Ee.js +0 -2
  74. package/remote/assets/vite-preload-helper-B7qeedMF.js +0 -1
  75. package/remote/assets/zh-De2Ilp0M.js +0 -1
  76. package/remote/remoteEntry.js +0 -1
  77. package/remote/remoteEntry.ssr.js +0 -1
package/README.md CHANGED
@@ -1,39 +1,59 @@
1
1
  # @incorta/nexus-chat
2
2
 
3
- NexusChat, the IncortaCopilot chat UI, for any app on any framework. One package holds:
4
-
5
- | Import | What |
6
- | ------------------------------------------ | ----------------------------------------------------------- |
7
- | `@incorta/nexus-chat` | the connector (`mountNexusChat`, `loadNexusChat`) and types |
8
- | `@incorta/nexus-chat/react` | `<NexusChat />` for React 18 and 19 apps |
9
- | `@incorta/nexus-chat/contract` | the host contract's values (`NEXUS_CHAT_CONTRACT_VERSION`…) |
10
- | `@incorta/nexus-chat/conformance` | checks a host bridge against the contract |
11
- | `@incorta/nexus-chat/host` | host helpers (`createElementRootAdapter`…) |
12
- | `node_modules/@incorta/nexus-chat/remote/` | the prebuilt chat, for apps that serve a pinned copy |
13
-
14
- The chat always runs as that prebuilt Module Federation bundle with its own React and isolated
3
+ NexusChat, the IncortaCopilot chat UI, for any app on any framework: `<NexusChat />` for React,
4
+ `mountNexusChat()` for everything else.
5
+
6
+ The chat always runs as a prebuilt Module Federation bundle with its own React and isolated
15
7
  styles, never re-compiled by the app's bundler, so it never collides with the app's React, its
16
- dependencies or its CSS. Importing the connector adds about 2 KB to an app; `remote/` stays on
17
- disk unless the app serves it.
8
+ dependencies or its CSS. Importing the connector and the component adds about 3 KB to an app. The
9
+ chat build itself is not in this package: Copilot serves it, and each GitHub Release attaches it as
10
+ `nexus-chat-dist.tar.gz`.
18
11
 
19
12
  ## React
20
13
 
14
+ Two ways in: **direct**, where the chat mounts into the page and shares its session (Incorta's own
15
+ UIs), and **embedded**, where it runs in an iframe served by Incorta on the user's Incorta session,
16
+ so the app never holds a token (generated apps, other sites).
17
+
18
+ ### Embedded
19
+
21
20
  ```tsx
22
21
  import { NexusChat } from '@incorta/nexus-chat/react';
23
22
 
23
+ <NexusChat embedUrl={`${incortaUrl}/copilot/nexus-chat/embed.html`} theme="dark" />;
24
+ ```
25
+
26
+ It draws its own window: an Incorta Intelligence launcher at the bottom right opens the chat
27
+ floating, docked on the right (the page makes room) or fullscreen, switched or minimized back to
28
+ the launcher from the top right. `modes` limits the windows (default all three, in that order);
29
+ `defaultMode` opens one on load. Below 768px wide the chat covers the page. The window takes the
30
+ page's background colour, the chat is drawn smaller when floating or docked, and the launcher icon
31
+ shimmers while the chat loads. `theme` is followed live; `locale` is read when the chat first opens.
32
+
33
+ The Incorta cluster needs `iframe.inclusion.enabled` (and `frame-ancestors` set to the app's
34
+ domains), and the user signed in to Incorta in that browser. The iframe reaches the Incorta
35
+ session only where the browser allows third-party cookies: Chrome by default; Safari ("Prevent
36
+ cross-site tracking" off), Firefox (a cookie exception for the Incorta site) and private windows
37
+ need that setting.
38
+
39
+ ### Direct
40
+
41
+ ```tsx
24
42
  <NexusChat
25
- connection={{ client: 'general', copilotUrl: '/api/ai', credentials: 'same-origin' }}
43
+ connection={{ client: 'general', copilotUrl: '/incorta/copilot', credentials: 'include' }}
26
44
  theme="dark"
27
45
  user={{ id: user.sub, name: user.name }}
28
46
  onLoadError={(error) => setError(error.message)}
29
- />;
47
+ />
30
48
  ```
31
49
 
32
- The chat fills the component's box. `theme`, `locale` (default: the browser's), `direction`,
33
- `tenantId`, `user` and `window` update the running chat; `connection`, `chat`, `actions`, `events`,
34
- `instanceId`, `assetsUrl` and `timeoutMs` are read once at mount. `window` (`docked`, `canDock`,
35
- `setDocked`, `close`) hands the chat's window to the app: the chat shows a dock toggle and a close
36
- button. `onReady` receives the handle (`ask`, `attach`).
50
+ The chat fills the component's box; the app draws any frame around it. `theme`, `locale` (default:
51
+ the browser's), `direction`, `tenantId`, `user` and `window` update the running chat;
52
+ `connection`, `chat`, `actions`, `events`, `instanceId`, `assetsUrl` and `timeoutMs` are read once
53
+ at mount. `window` (`docked`, `canDock`, `setDocked`, `close`) hands the chat's window to the app:
54
+ the chat shows a dock toggle and a close button. An app that already builds the contract's
55
+ `environment` or `window` passes it as is. `onReady` receives the handle (`ask`, `attach`), and
56
+ `null` once the chat unmounts; `loadedNexusChatVersion()` names the loaded build for error reports.
37
57
 
38
58
  ## Any framework
39
59
 
@@ -55,35 +75,40 @@ The options are the host contract: `connection` and `environment` are required;
55
75
  `events` and `window` are optional, and the chat hides what a host leaves out. Mount in the browser
56
76
  only, never during SSR.
57
77
 
78
+ `chat={{ contexts: ['business-view', 'ai-container', 'file'] }}` lets users attach contexts, in
79
+ that menu order: `'file'` uploads, and each other built-in kind (`business-view`, `physical-table`,
80
+ `ai-container`, `knowledge-store`) is loaded by the chat from Incorta through
81
+ `connection.incortaApi` (for example, `{ type: 'bff', baseUrl: '/incorta/bff/v1' }`). Without
82
+ `incortaApi`, a bare kind is left out with a console warning; the public API has no Knowledge
83
+ Stores. A `ContextSource` of a kind replaces the chat's loader for it.
84
+
58
85
  `loadNexusChat(source)` only loads the chat and resolves to its `mount`. It loads once per source,
59
86
  retries once, and gives up after `timeoutMs` (default 20 s); a failed load is forgotten so the next
60
87
  call tries again. Call it when the page is idle to make the first open instant.
61
88
 
62
89
  ## Where the chat comes from
63
90
 
64
- | Mode | Options | UI version | App setup |
65
- | ------------------------- | ----------------------------------------------------------------------------- | ----------------------------- | ----------------------------------------------- |
66
- | Always updated (default) | `connection.copilotUrl` only | what that Copilot runs | none: Copilot serves `<copilotUrl>/nexus-chat/` |
67
- | Pinned, served by the app | `assetsUrl: '/nexus-chat/'` | the installed package version | serve this package's `remote/` at that path |
68
- | Pinned, served from a CDN | `assetsUrl: 'https://cdn.jsdelivr.net/npm/@incorta/nexus-chat@1.4.2/remote/'` | the version in the URL | none; the page's CSP must allow that origin |
91
+ | Mode | Options | UI version | App setup |
92
+ | ------------------------ | ---------------------------- | ------------------------- | ---------------------------------------------------------------------- |
93
+ | Always updated (default) | `connection.copilotUrl` only | what that Copilot runs | none: Copilot serves `<copilotUrl>/nexus-chat/` |
94
+ | Pinned | `assetsUrl: '/nexus-chat/'` | the release you extracted | serve that release's `nexus-chat-dist.tar.gz`, extracted, at that path |
69
95
 
70
- An app with a server serves `remote/` straight from the installed package:
96
+ A pinned copy serves `remoteEntry.js` with revalidation (`no-cache`) and `assets/` as immutable.
71
97
 
72
- ```ts
73
- import { createRequire } from 'node:module';
74
- import path from 'node:path';
75
-
76
- const remoteDir = path.join(
77
- path.dirname(createRequire(import.meta.url).resolve('@incorta/nexus-chat/package.json')),
78
- 'remote',
79
- );
80
- // serve remoteDir at /nexus-chat/: remoteEntry.js revalidates (no-cache), assets/ is immutable
81
- ```
98
+ A pinned UI keeps talking to a Copilot that keeps moving; the default mode keeps the two in step.
82
99
 
83
- A static app copies it instead:
84
- `"postinstall": "cp -R node_modules/@incorta/nexus-chat/remote public/nexus-chat"`.
100
+ ## Everything in the package
85
101
 
86
- A pinned UI keeps talking to a Copilot that keeps moving; the default mode keeps the two in step.
102
+ React and other apps need only the first two imports; the rest serve hosts that federate the chat
103
+ themselves (web-react, Hikari).
104
+
105
+ | Import | What |
106
+ | --------------------------------- | ----------------------------------------------------------- |
107
+ | `@incorta/nexus-chat` | the connector (`mountNexusChat`, `loadNexusChat`) and types |
108
+ | `@incorta/nexus-chat/react` | `<NexusChat />` for React 18 and 19 apps |
109
+ | `@incorta/nexus-chat/contract` | the host contract's values (`NEXUS_CHAT_CONTRACT_VERSION`…) |
110
+ | `@incorta/nexus-chat/conformance` | checks a host bridge against the contract |
111
+ | `@incorta/nexus-chat/host` | host helpers (`createElementRootAdapter`…) |
87
112
 
88
113
  ## Versions
89
114
 
@@ -1,4 +1,4 @@
1
- import { NEXUS_CHAT_CONTRACT_VERSION, } from './index.js';
1
+ import { BUILT_IN_CONTEXT_KINDS, NEXUS_CHAT_CONTRACT_VERSION, } from './index.js';
2
2
  class CheckCollector {
3
3
  results = [];
4
4
  async run(id, title, check) {
@@ -255,15 +255,17 @@ export async function checkHostContract(options, checkOptions = {}) {
255
255
  ensure(notifications === before, 'unsubscribe did not detach the listener.');
256
256
  }
257
257
  });
258
- await checks.run('chat.contexts', 'each context is "file" or a source with a loader', () => {
258
+ await checks.run('chat.contexts', 'each context is a built-in kind or a source with a loader', () => {
259
259
  const kinds = (chat?.contexts ?? []).map((spec) => {
260
- if (spec === 'file')
260
+ if (typeof spec === 'string') {
261
+ ensure(BUILT_IN_CONTEXT_KINDS.includes(spec), `unknown context ${JSON.stringify(spec)}; a bare string is one of the built-in kinds: ${BUILT_IN_CONTEXT_KINDS.join(', ')}.`);
261
262
  return spec;
263
+ }
262
264
  ensure(CONTEXT_KINDS.includes(spec.kind), `unknown context kind ${JSON.stringify(spec.kind)}.`);
263
265
  ensure(typeof spec.load === 'function', `the ${spec.kind} source has no load().`);
264
266
  return spec.kind;
265
267
  });
266
- ensure(new Set(kinds).size === kinds.length, 'a context kind is listed twice.');
268
+ ensure(new Set(kinds).size === kinds.length, 'a context kind is listed twice (a source replaces its bare kind: list only the source).');
267
269
  });
268
270
  await checks.run('chat.triggers', 'each trigger is @ or # with a search', () => {
269
271
  const chars = (chat?.triggers ?? []).map((trigger) => {
@@ -22,6 +22,17 @@ export interface NexusChatConnection {
22
22
  * Absent: the failure surfaces to the user.
23
23
  */
24
24
  refreshAuth?(): Promise<boolean>;
25
+ /**
26
+ * How the chat reaches the Incorta API for built-in kinds other than `'file'` in
27
+ * `chat.contexts`, under the headers, credentials and `refreshAuth` above.
28
+ * - `'bff'`: Incorta's internal BFF (web-react: `baseUrl: '/incorta/bff/v1'`;
29
+ * the session headers carry the user).
30
+ * - `'public'`: the public API v2, rooted at `/api/v2/{tenant}`.
31
+ */
32
+ incortaApi?: {
33
+ type: 'bff' | 'public';
34
+ baseUrl: string;
35
+ };
25
36
  /** Poll tuning; hosts normally omit both (defaults 1500 ms / 1000 ms). */
26
37
  pollIntervalMs?: number;
27
38
  initialPollDelayMs?: number;
@@ -80,8 +91,34 @@ export interface ContextSource {
80
91
  */
81
92
  resolve?(dataset: Readonly<Record<string, unknown>>, signal: AbortSignal): Promise<Readonly<Record<string, unknown>>>;
82
93
  }
83
- /** `'file'` is the chat's own upload browser; every other kind is a host source. */
84
- export type ContextSpec = 'file' | ContextSource;
94
+ /**
95
+ * The built-in kinds the chat handles itself: its upload browser and contexts
96
+ * loaded through `connection.incortaApi`. `'insight'` is not one: insights come
97
+ * from the dashboard the host has open, so only a host source can list them.
98
+ */
99
+ export declare const BUILT_IN_CONTEXT_KINDS: readonly ["file", "business-view", "physical-table", "ai-container", "knowledge-store"];
100
+ export type BuiltInContextKind = (typeof BUILT_IN_CONTEXT_KINDS)[number];
101
+ /**
102
+ * One entry of the add-context menu; `chat.contexts` lists them in menu order.
103
+ *
104
+ * - `'file'`: the chat's own upload browser.
105
+ * - Another built-in kind (`'business-view'`, …): the chat loads it from
106
+ * `connection.incortaApi`. Without `incortaApi`, or for a kind its API does not
107
+ * offer (`'knowledge-store'` has no public API), the kind is left out of
108
+ * the menu with a console warning.
109
+ * - A `ContextSource`: the host loads the rows. A source of a built-in kind
110
+ * replaces the chat's loader for that kind.
111
+ *
112
+ * Name each kind once. A host that lists a bare kind and a source of the same
113
+ * kind fails conformance; the chat still shows the kind once, as the host's
114
+ * source, in the source's place. Any other string is dropped.
115
+ *
116
+ * Migrating: code that told a source apart with `spec === 'file'` no longer
117
+ * compiles, since a bare string may now be a kind; narrow with
118
+ * `typeof spec === 'string'`. Pass bare kinds only to a chat that has them
119
+ * (this release or later): an older chat cannot render them.
120
+ */
121
+ export type ContextSpec = BuiltInContextKind | ContextSource;
85
122
  export interface ColumnSummary {
86
123
  id: string;
87
124
  name: string;
package/contract/index.js CHANGED
@@ -1,4 +1,16 @@
1
1
  export const NEXUS_CHAT_CONTRACT_VERSION = 2;
2
+ /**
3
+ * The built-in kinds the chat handles itself: its upload browser and contexts
4
+ * loaded through `connection.incortaApi`. `'insight'` is not one: insights come
5
+ * from the dashboard the host has open, so only a host source can list them.
6
+ */
7
+ export const BUILT_IN_CONTEXT_KINDS = [
8
+ 'file',
9
+ 'business-view',
10
+ 'physical-table',
11
+ 'ai-container',
12
+ 'knowledge-store',
13
+ ];
2
14
  /**
3
15
  * Reads the namespaced-kind convention. Returns null for bare legacy kinds.
4
16
  * Framework-free on purpose: hosts use this inside `canRender` to route by
package/index.d.ts CHANGED
@@ -6,8 +6,8 @@ export interface NexusChatSource {
6
6
  /** IncortaCopilot's root as this page reaches it; the chat loads from `<copilotUrl>/nexus-chat/`. */
7
7
  copilotUrl?: string;
8
8
  /**
9
- * Where the chat files are served instead (pinned): the app's own copy of
10
- * this package's `remote/` folder, or a CDN URL. Absent: from Copilot.
9
+ * Where the chat files are served instead (pinned): any URL that serves a
10
+ * release's extracted `nexus-chat-dist.tar.gz`. Absent: from Copilot.
11
11
  */
12
12
  assetsUrl?: string;
13
13
  /** Deadline for loading the chat, retry included. Default 20 000 ms. */
@@ -24,6 +24,12 @@ export const CONTRACT_VERSION: 2;
24
24
  /** Loads the chat once per source (one retry, one deadline) and resolves to its `mount`. */
25
25
  export function loadNexusChat(source: NexusChatSource): Promise<NexusChatMount>;
26
26
 
27
+ /** Forgets a loaded chat, so the next load fetches it again (after it failed while running). */
28
+ export function forgetNexusChat(source: NexusChatSource): void;
29
+
30
+ /** The version the last loaded chat reports (for error reports); undefined before a load. */
31
+ export function loadedNexusChatVersion(): string | undefined;
32
+
27
33
  /** Loads the chat and mounts it into `element`. */
28
34
  export function mountNexusChat(
29
35
  element: HTMLElement,
package/index.js CHANGED
@@ -4,9 +4,8 @@
4
4
  //
5
5
  // - IncortaCopilot (default): `<copilotUrl>/nexus-chat/`, always the version
6
6
  // that Copilot runs, so the UI matches its backend;
7
- // - this package's own `remote/` folder, served by the app (pinned to the
8
- // installed version);
9
- // - any URL that serves a `remote/` folder, such as a CDN (pinned by its URL).
7
+ // - any URL that serves a release's chat build (`nexus-chat-dist.tar.gz` on
8
+ // the GitHub Release, extracted), pinned to that release.
10
9
  //
11
10
  // `assetsUrl` picks the source; without it the chat comes from Copilot.
12
11
  // The host contract ships alongside: its types here, its values and helpers
@@ -21,6 +20,9 @@ const DEFAULT_TIMEOUT_MS = 20_000;
21
20
 
22
21
  const loads = new Map();
23
22
  const attempts = new Map();
23
+ /** Stylesheets of the chat that failed to load, reloaded by the next attempt. */
24
+ const failedStylesheets = new Set();
25
+ let loadedVersion;
24
26
 
25
27
  function entryUrl({ copilotUrl, assetsUrl }) {
26
28
  const base = assetsUrl ?? (copilotUrl && `${copilotUrl.replace(/\/+$/, '')}/nexus-chat/`);
@@ -28,11 +30,74 @@ function entryUrl({ copilotUrl, assetsUrl }) {
28
30
  return new URL('remoteEntry.js', new URL(base, window.location.href)).href;
29
31
  }
30
32
 
33
+ // A failed CSS chunk rejects the load, and Vite's preload never asks for a
34
+ // sheet it has seen again. Remember the chat's failed sheets so the next
35
+ // attempt requests them anew. A sheet failing again fails that attempt but is
36
+ // forgotten, so one a deploy removed blocks a single attempt, not every one.
37
+ function captureFailedStylesheets(base) {
38
+ if (captureFailedStylesheets.installed) return;
39
+ captureFailedStylesheets.installed = true;
40
+ window.addEventListener(
41
+ 'error',
42
+ ({ target }) => {
43
+ if (!(target instanceof HTMLLinkElement) || target.rel !== 'stylesheet') return;
44
+ if (target.dataset.nexusChatRetry === undefined && target.href.startsWith(base)) {
45
+ failedStylesheets.add(target.href);
46
+ }
47
+ },
48
+ true,
49
+ );
50
+ }
51
+
52
+ function reloadFailedStylesheets() {
53
+ const reloads = [...failedStylesheets].map((href) => {
54
+ failedStylesheets.delete(href);
55
+ document.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
56
+ if (link.href === href) link.remove();
57
+ });
58
+ return new Promise((resolve, reject) => {
59
+ const link = Object.assign(document.createElement('link'), { rel: 'stylesheet', href });
60
+ link.dataset.nexusChatRetry = '';
61
+ link.onload = resolve;
62
+ link.onerror = () => {
63
+ link.remove();
64
+ reject(new Error(`NexusChat stylesheet failed to load: ${href}`));
65
+ };
66
+ document.head.append(link);
67
+ });
68
+ });
69
+ return Promise.all(reloads);
70
+ }
71
+
72
+ // A chat served by its Vite dev server ships React Fast Refresh code, which
73
+ // throws unless the page ran @vitejs/plugin-react's preamble. Only a local
74
+ // dev server can be one, so only there is it probed (GET; Vite ignores HEAD).
75
+ async function installReactRefreshPreamble(base) {
76
+ if (window.__vite_plugin_react_preamble_installed__) return;
77
+ if (!/^(localhost|127\.0\.0\.1|\[::1\])$|\.localhost$/.test(new URL(base).hostname)) return;
78
+ try {
79
+ const url = new URL('@react-refresh', base).href;
80
+ const probe = await fetch(url, { signal: AbortSignal.timeout(3_000) });
81
+ if (!probe.ok || !probe.headers.get('content-type')?.includes('javascript')) return;
82
+ const runtime = await import(/* @vite-ignore */ /* webpackIgnore: true */ url);
83
+ runtime.default.injectIntoGlobalHook(window);
84
+ window.$RefreshReg$ = () => {};
85
+ window.$RefreshSig$ = () => (type) => type;
86
+ window.__vite_plugin_react_preamble_installed__ = true;
87
+ } catch {
88
+ // Not a dev-served chat.
89
+ }
90
+ }
91
+
31
92
  async function importMount(entry) {
32
93
  // The browser remembers a failed import by URL, so every attempt after the
33
94
  // first asks for a new one, or it would never reach the network again.
34
95
  const attempt = attempts.get(entry) ?? 0;
35
96
  attempts.set(entry, attempt + 1);
97
+ const base = new URL('.', entry).href;
98
+ captureFailedStylesheets(base);
99
+ await installReactRefreshPreamble(base);
100
+ if (attempt) await reloadFailedStylesheets();
36
101
  const url = new URL(entry);
37
102
  if (attempt) url.searchParams.set('nexus-chat-attempt', String(attempt));
38
103
  const remote = await import(/* @vite-ignore */ /* webpackIgnore: true */ url.href);
@@ -42,9 +107,25 @@ async function importMount(entry) {
42
107
  if (typeof module?.default !== 'function') {
43
108
  throw new Error('NexusChat remote did not expose a mount function.');
44
109
  }
110
+ loadedVersion =
111
+ typeof module.NEXUS_CHAT_VERSION === 'string' ? module.NEXUS_CHAT_VERSION : undefined;
45
112
  return module.default;
46
113
  }
47
114
 
115
+ /**
116
+ * Forgets a loaded chat, so the next load fetches it again: after it failed
117
+ * while running (a redeploy can leave it asking for files that are gone).
118
+ */
119
+ export function forgetNexusChat(source) {
120
+ // A source with no URL was never loaded.
121
+ if (source.copilotUrl || source.assetsUrl) loads.delete(entryUrl(source));
122
+ }
123
+
124
+ /** The version the last loaded chat reports (for error reports); undefined before a load. */
125
+ export function loadedNexusChatVersion() {
126
+ return loadedVersion;
127
+ }
128
+
48
129
  /**
49
130
  * Loads the chat once per source and resolves to its `mount` function. One
50
131
  * retry, since a single failure (a 502 during a rolling restart) is not an
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ahmedramez-org/nexus-chat",
3
- "version": "1.0.0-beta.1",
4
- "description": "NexusChat for any app: a connector and React component that load the prebuilt chat from IncortaCopilot (or a pinned copy), the host contract types and checks, and the chat bundle itself.",
3
+ "version": "1.0.0-beta.3",
4
+ "description": "NexusChat for any app: a connector and React component that load the prebuilt chat from IncortaCopilot (or a pinned copy), and the host contract types and checks.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
7
7
  "type": "git",
@@ -31,7 +31,6 @@
31
31
  "types": "./contract/host.d.ts",
32
32
  "default": "./contract/host.js"
33
33
  },
34
- "./remote/*": "./remote/*",
35
34
  "./package.json": "./package.json"
36
35
  },
37
36
  "files": [
@@ -40,7 +39,6 @@
40
39
  "react.js",
41
40
  "react.d.ts",
42
41
  "contract",
43
- "remote",
44
42
  "README.md"
45
43
  ],
46
44
  "publishConfig": {
package/react.d.ts CHANGED
@@ -5,38 +5,74 @@ import type {
5
5
  HostTheme,
6
6
  HostUser,
7
7
  MountNexusChatOptions,
8
+ NexusChatEnvironment,
8
9
  NexusChatHandle,
10
+ NexusChatWindow,
9
11
  } from './index.js';
10
12
 
11
- export interface NexusChatProps extends Omit<MountNexusChatOptions, 'environment' | 'window'> {
12
- theme: HostTheme;
13
+ /** The chat mounted into this page, sharing its session. It fills its box. */
14
+ export interface DirectNexusChatProps extends Omit<
15
+ MountNexusChatOptions,
16
+ 'environment' | 'window'
17
+ > {
18
+ embedUrl?: undefined;
19
+ /** Required unless `environment` is given. */
20
+ theme?: HostTheme;
13
21
  /** Default: the browser's language. */
14
22
  locale?: string;
15
23
  /** Absent: follows `locale`. */
16
24
  direction?: HostDirection;
17
25
  tenantId?: string;
18
26
  user?: HostUser;
27
+ /** The app's own contract environment, used as is instead of the props above. */
28
+ environment?: NexusChatEnvironment;
19
29
  /**
20
30
  * The app owns the chat's window (a docked panel, an overlay): the chat
21
- * shows a dock toggle while `canDock`, and a close button.
31
+ * shows a dock toggle while `canDock`, and a close button. A contract
32
+ * `NexusChatWindow` is used as is.
22
33
  */
23
- window?: {
24
- docked: boolean;
25
- canDock: boolean;
26
- setDocked(docked: boolean): void;
27
- close(): void;
28
- };
29
- /** The chat is mounted: `ask` and `attach` are available. */
30
- onReady?(chat: NexusChatHandle): void;
34
+ window?:
35
+ | {
36
+ docked: boolean;
37
+ canDock: boolean;
38
+ setDocked(docked: boolean): void;
39
+ close(): void;
40
+ }
41
+ | NexusChatWindow;
42
+ /** The chat is mounted (`ask` and `attach` are available); `null` once it unmounts. */
43
+ onReady?(chat: NexusChatHandle | null): void;
31
44
  /** The chat could not load (after its retry and deadline). */
32
45
  onLoadError?(error: Error): void;
33
46
  className?: string;
34
47
  style?: CSSProperties;
35
48
  }
36
49
 
50
+ export type NexusChatMode = 'floating' | 'docked' | 'fullscreen';
51
+
52
+ /**
53
+ * The chat in an <iframe> served by Incorta, on the user's own Incorta
54
+ * session (the app never holds a token). It draws its window: a launcher
55
+ * icon that opens the chat floating, docked on the right, or fullscreen.
56
+ */
57
+ export interface EmbeddedNexusChatProps {
58
+ /** Incorta's chat page: `<incorta>/copilot/nexus-chat/embed.html`. */
59
+ embedUrl: string;
60
+ /** Where the chat may open, in launcher order. Default: all three. */
61
+ modes?: NexusChatMode[];
62
+ /** Open on load in this mode. Default: the launcher icon only. */
63
+ defaultMode?: NexusChatMode;
64
+ /** Default `'light'`; followed live. */
65
+ theme?: HostTheme;
66
+ /** Read once, when the chat first opens. Default: the browser's language. */
67
+ locale?: string;
68
+ }
69
+
70
+ export type NexusChatProps = DirectNexusChatProps | EmbeddedNexusChatProps;
71
+
37
72
  /**
38
- * The chat as a React component. It fills its box. `connection`, `chat`,
39
- * `actions`, `events`, `instanceId`, `assetsUrl` and `timeoutMs` are read
40
- * once at mount; the other props update the running chat.
73
+ * The chat as a React component. Direct: it fills its box; `connection`,
74
+ * `chat`, `actions`, `events`, `instanceId`, `assetsUrl` and `timeoutMs` are
75
+ * read once at mount, and the other props update the running chat.
76
+ * Embedded (`embedUrl`): it draws its own window.
41
77
  */
42
78
  export function NexusChat(props: NexusChatProps): ReactElement;