@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.
- package/README.md +65 -40
- package/contract/conformance.js +6 -4
- package/contract/index.d.ts +39 -2
- package/contract/index.js +12 -0
- package/index.d.ts +8 -2
- package/index.js +84 -3
- package/package.json +2 -4
- package/react.d.ts +50 -14
- package/react.js +367 -37
- 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-DL02k5Y5.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-DgWzju8R.js +0 -1
- package/remote/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__nexusChat__remoteEntry_js-BCqsZDEx.js +0 -1
- package/remote/assets/virtual_mf-exposes-ssr___mfe_internal__nexusChat__remoteEntry_js-nszk7xTq.js +0 -2
- package/remote/assets/virtual_mf-exposes___mfe_internal__nexusChat__remoteEntry_js-BkoNn4Ee.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/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
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
17
|
-
|
|
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: '/
|
|
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:
|
|
33
|
-
`tenantId`, `user` and `window` update the running chat;
|
|
34
|
-
`instanceId`, `assetsUrl` and `timeoutMs` are read once
|
|
35
|
-
`setDocked`, `close`) hands the chat's window to the app:
|
|
36
|
-
button.
|
|
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
|
|
65
|
-
|
|
|
66
|
-
| Always updated (default)
|
|
67
|
-
| Pinned
|
|
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
|
-
|
|
96
|
+
A pinned copy serves `remoteEntry.js` with revalidation (`no-cache`) and `assets/` as immutable.
|
|
71
97
|
|
|
72
|
-
|
|
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
|
-
|
|
84
|
-
`"postinstall": "cp -R node_modules/@incorta/nexus-chat/remote public/nexus-chat"`.
|
|
100
|
+
## Everything in the package
|
|
85
101
|
|
|
86
|
-
|
|
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
|
|
package/contract/conformance.js
CHANGED
|
@@ -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
|
|
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 === '
|
|
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) => {
|
package/contract/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
84
|
-
|
|
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):
|
|
10
|
-
*
|
|
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
|
-
// -
|
|
8
|
-
//
|
|
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.
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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.
|
|
39
|
-
* `actions`, `events`, `instanceId`, `assetsUrl` and `timeoutMs` are
|
|
40
|
-
* once at mount
|
|
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;
|