@ahmedramez-org/nexus-chat 1.0.0-beta.2 → 1.0.0-beta.20
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 +240 -35
- package/window.js +383 -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/README.md
CHANGED
|
@@ -1,31 +1,127 @@
|
|
|
1
1
|
# @incorta/nexus-chat
|
|
2
2
|
|
|
3
|
-
NexusChat, the IncortaCopilot chat UI, for any app on any framework: `<
|
|
4
|
-
`mountNexusChat()` for everything else.
|
|
3
|
+
NexusChat, the IncortaCopilot chat UI, for any app on any framework: one `<script>` tag for any
|
|
4
|
+
site, `<NexusChat />` for React, `mountNexusChat()` for everything else.
|
|
5
5
|
|
|
6
6
|
The chat always runs as a prebuilt Module Federation bundle with its own React and isolated
|
|
7
7
|
styles, never re-compiled by the app's bundler, so it never collides with the app's React, its
|
|
8
|
-
dependencies or its CSS.
|
|
9
|
-
|
|
8
|
+
dependencies or its CSS. The connector adds about 6 KB gzipped to an app (half of it the Incorta
|
|
9
|
+
Intelligence mark its loading skeleton draws), and the React component about 7 KB more. The chat
|
|
10
|
+
build itself is not in this package: Copilot serves it, and each GitHub Release attaches it as
|
|
11
|
+
`nexus-chat-dist.tar.gz`.
|
|
12
|
+
|
|
13
|
+
## Any site: one script
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<script src="https://<incorta>/incorta/copilot/nexus-chat/widget.js" defer></script>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Incorta serves this script next to `embed.html`; it is not in this package and needs no build. It
|
|
20
|
+
draws the Incorta Intelligence launcher at the bottom right and opens the embedded chat (below)
|
|
21
|
+
floating, docked or full screen, on the user's Incorta session, so the page holds no token. It is
|
|
22
|
+
one classic script with no imports, so a page on any origin loads it without CORS.
|
|
23
|
+
|
|
24
|
+
Optional `data-` attributes set it up: `data-theme="dark"`, `data-modes="floating,docked"` (the
|
|
25
|
+
windows allowed, in that order; default all three), `data-default-mode="docked"` (open on load;
|
|
26
|
+
default only the launcher) and `data-locale="fr"`. The page drives it afterwards through
|
|
27
|
+
`window.NexusChatWidget`: `open()`, `close()`, `setMode('docked')`, `setTheme('dark')`.
|
|
10
28
|
|
|
11
29
|
## React
|
|
12
30
|
|
|
31
|
+
Two ways in: **direct**, where the chat mounts into the page and shares its session (Incorta's own
|
|
32
|
+
UIs), and **embedded**, where it runs in an iframe served by Incorta on the user's Incorta session,
|
|
33
|
+
so the app never holds a token (generated apps, other sites).
|
|
34
|
+
|
|
35
|
+
### Embedded
|
|
36
|
+
|
|
13
37
|
```tsx
|
|
14
38
|
import { NexusChat } from '@incorta/nexus-chat/react';
|
|
15
39
|
|
|
40
|
+
<NexusChat embedUrl={`${incortaUrl}/copilot/nexus-chat/embed.html`} theme="dark" />;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
It draws its own window: an Incorta Intelligence launcher at the bottom right opens the chat
|
|
44
|
+
floating (a card lifted off the page, opening out of the launcher, which stays under it to hide
|
|
45
|
+
it) or docked flush on the right (the page makes room). The chat's own top bar switches between
|
|
46
|
+
the windows (Float, Dock to the side, Enter full screen: every allowed one but the current) and
|
|
47
|
+
closes it back to the launcher; nothing sits above the chat. `modes` limits the windows (default
|
|
48
|
+
all three, in that order) and `defaultMode` opens one on load. Below 768px wide the chat covers
|
|
49
|
+
the page. The chat is drawn smaller in a small window, and a skeleton holds the window while the
|
|
50
|
+
chat loads. `theme` is followed live; `locale` is read when the chat first opens.
|
|
51
|
+
|
|
52
|
+
The Incorta cluster needs `iframe.inclusion.enabled` (and `frame-ancestors` set to the app's
|
|
53
|
+
domains). Where the browser keeps the Incorta session from the iframe (Safari, Firefox, private
|
|
54
|
+
windows), the chat shows Continue with Incorta: the browser asks the user to allow it once, and
|
|
55
|
+
the chat starts on their Incorta session. A user not signed in to Incorta signs in through a small
|
|
56
|
+
Incorta window that closes itself afterwards. No browser setting is needed.
|
|
57
|
+
|
|
58
|
+
While the user clicks or types in the app, the component tells the chat at most every ten seconds,
|
|
59
|
+
so Incorta's own idle timeout does not end the chat's session while the app is in use. A page that
|
|
60
|
+
places the iframe itself can do the same: `iframe.contentWindow.postMessage({ nexusChat: 'active' },
|
|
61
|
+
'<incorta origin>')`. If the session ends anyway, the chat shows Sign in to Incorta.
|
|
62
|
+
|
|
63
|
+
### Direct
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// Copilot answers 400 without the signed-in user's token: read it from Incorta's
|
|
67
|
+
// session, and read it again after a 401 or 403.
|
|
68
|
+
let session;
|
|
69
|
+
const incortaHeaders = () =>
|
|
70
|
+
(session ??= fetch('/incorta/service/user/isLoggedIn', { credentials: 'include' }).then(
|
|
71
|
+
async (response) => ({
|
|
72
|
+
Authorization: `Bearer ${(await response.json()).accessToken}`,
|
|
73
|
+
'X-XSRF-TOKEN': response.headers.get('X-XSRF-TOKEN') ?? '',
|
|
74
|
+
}),
|
|
75
|
+
));
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
16
79
|
<NexusChat
|
|
17
|
-
connection={{
|
|
80
|
+
connection={{
|
|
81
|
+
client: 'general',
|
|
82
|
+
copilotUrl: '/incorta/copilot',
|
|
83
|
+
credentials: 'include',
|
|
84
|
+
headers: incortaHeaders, // read before each request
|
|
85
|
+
refreshAuth: async () => ((session = undefined), true), // after a 401 or 403: read again, retry
|
|
86
|
+
}}
|
|
18
87
|
theme="dark"
|
|
19
88
|
user={{ id: user.sub, name: user.name }}
|
|
20
89
|
onLoadError={(error) => setError(error.message)}
|
|
21
|
-
|
|
90
|
+
/>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The chat fills the component's box; the app draws any frame around it. Until the chat is mounted, a skeleton holds the box (the embedded window's loading face: the Incorta Intelligence mark and two lines, after 300 ms), and it fades once the chat mounts, or leaves at once when `onLoadError` fires, so the app's own failure takes the box. `theme`, `locale` (default:
|
|
94
|
+
the browser's), `direction`, `tenantId`, `user` and `window` update the running chat;
|
|
95
|
+
`connection`, `chat`, `actions`, `events`, `instanceId`, `assetsUrl` and `timeoutMs` are read once
|
|
96
|
+
at mount. `window` (`docked`, `canDock`, `setDocked`, `close`) hands the chat's window to the app:
|
|
97
|
+
the chat shows a dock toggle and a close button; with `mode`, `modes` and `setMode` (read at
|
|
98
|
+
mount) it shows a button per other mode instead of the toggle. An app that already builds the contract's
|
|
99
|
+
`environment` or `window` passes it as is. `onReady` receives the handle (`ask`, `attach`), and
|
|
100
|
+
`null` once the chat unmounts; `loadedNexusChatVersion()` names the loaded build for error reports.
|
|
101
|
+
|
|
102
|
+
### Direct, in a window
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<NexusChat
|
|
106
|
+
connection={{
|
|
107
|
+
client: 'general',
|
|
108
|
+
copilotUrl: '/incorta/copilot',
|
|
109
|
+
credentials: 'include',
|
|
110
|
+
headers: incortaHeaders, // as above
|
|
111
|
+
refreshAuth: async () => ((session = undefined), true),
|
|
112
|
+
}}
|
|
113
|
+
theme="dark"
|
|
114
|
+
modes={['floating', 'docked', 'fullscreen']}
|
|
115
|
+
defaultMode="floating"
|
|
116
|
+
/>
|
|
22
117
|
```
|
|
23
118
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
119
|
+
`modes` or `defaultMode` instead of `window` gives the direct chat the embedded window: the
|
|
120
|
+
launcher, floating, docked (the page makes room) and full-screen windows, full screen below 768px.
|
|
121
|
+
The chat's own top bar switches windows and closes it to the launcher, where the conversation
|
|
122
|
+
stays. `modes` limits the windows (default all three, in that order); `defaultMode` opens one on
|
|
123
|
+
load, else only the launcher shows. The other props are the direct ones, and an app passing
|
|
124
|
+
`window` keeps drawing its own frame.
|
|
29
125
|
|
|
30
126
|
## Any framework
|
|
31
127
|
|
|
@@ -44,52 +140,46 @@ chat.unmount();
|
|
|
44
140
|
```
|
|
45
141
|
|
|
46
142
|
The options are the host contract: `connection` and `environment` are required; `chat`, `actions`,
|
|
47
|
-
`events` and `window` are optional, and the chat hides what a host leaves out.
|
|
48
|
-
|
|
143
|
+
`events` and `window` are optional, and the chat hides what a host leaves out. The chat fills
|
|
144
|
+
`element`. Until it is mounted the React component's skeleton holds the element (after 300 ms, in
|
|
145
|
+
the `environment`'s theme), and it leaves at once if the chat cannot load, so the app's own failure
|
|
146
|
+
takes its place. Mount in the browser only, never during SSR.
|
|
147
|
+
|
|
148
|
+
`chat={{ contexts: ['business-view', 'ai-container', 'file'] }}` lets users attach contexts, in
|
|
149
|
+
that menu order: `'file'` uploads, and each other built-in kind (`business-view`, `physical-table`,
|
|
150
|
+
`ai-container`, `knowledge-store`) is loaded by the chat from Incorta through
|
|
151
|
+
`connection.incortaApi` (for example, `{ type: 'bff', baseUrl: '/incorta/bff/v1' }`). Without
|
|
152
|
+
`incortaApi`, a bare kind is left out with a console warning; the public API has no Knowledge
|
|
153
|
+
Stores. A `ContextSource` of a kind replaces the chat's loader for it.
|
|
49
154
|
|
|
50
155
|
`loadNexusChat(source)` only loads the chat and resolves to its `mount`. It loads once per source,
|
|
51
|
-
retries once, and gives up after `timeoutMs` (default 20 s
|
|
156
|
+
retries once, and gives up after `timeoutMs` (default 20 s, counted while the page is visible: a
|
|
157
|
+
hidden tab gets a fresh deadline once it shows); a failed load is forgotten so the next
|
|
52
158
|
call tries again. Call it when the page is idle to make the first open instant.
|
|
53
159
|
|
|
54
160
|
## Where the chat comes from
|
|
55
161
|
|
|
56
|
-
| Mode
|
|
57
|
-
|
|
|
58
|
-
| Always updated (default)
|
|
59
|
-
| Pinned
|
|
60
|
-
| 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 |
|
|
61
|
-
|
|
62
|
-
An app with a server serves `remote/` straight from the installed package:
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
import { createRequire } from 'node:module';
|
|
66
|
-
import path from 'node:path';
|
|
67
|
-
|
|
68
|
-
const remoteDir = path.join(
|
|
69
|
-
path.dirname(createRequire(import.meta.url).resolve('@incorta/nexus-chat/package.json')),
|
|
70
|
-
'remote',
|
|
71
|
-
);
|
|
72
|
-
// serve remoteDir at /nexus-chat/: remoteEntry.js revalidates (no-cache), assets/ is immutable
|
|
73
|
-
```
|
|
162
|
+
| Mode | Options | UI version | App setup |
|
|
163
|
+
| ------------------------ | ---------------------------- | ------------------------- | ---------------------------------------------------------------------- |
|
|
164
|
+
| Always updated (default) | `connection.copilotUrl` only | what that Copilot runs | none: Copilot serves `<copilotUrl>/nexus-chat/` |
|
|
165
|
+
| Pinned | `assetsUrl: '/nexus-chat/'` | the release you extracted | serve that release's `nexus-chat-dist.tar.gz`, extracted, at that path |
|
|
74
166
|
|
|
75
|
-
A
|
|
76
|
-
`"postinstall": "cp -R node_modules/@incorta/nexus-chat/remote public/nexus-chat"`.
|
|
167
|
+
A pinned copy serves `remoteEntry.js` with revalidation (`no-cache`) and `assets/` as immutable.
|
|
77
168
|
|
|
78
169
|
A pinned UI keeps talking to a Copilot that keeps moving; the default mode keeps the two in step.
|
|
79
170
|
|
|
80
171
|
## Everything in the package
|
|
81
172
|
|
|
82
173
|
React and other apps need only the first two imports; the rest serve hosts that federate the chat
|
|
83
|
-
themselves (web-react, Hikari)
|
|
84
|
-
|
|
85
|
-
| Import
|
|
86
|
-
|
|
|
87
|
-
| `@incorta/nexus-chat`
|
|
88
|
-
| `@incorta/nexus-chat/react`
|
|
89
|
-
| `@incorta/nexus-chat/contract`
|
|
90
|
-
| `@incorta/nexus-chat/conformance`
|
|
91
|
-
| `@incorta/nexus-chat/host`
|
|
92
|
-
| `node_modules/@incorta/nexus-chat/remote/` | the prebuilt chat, for apps that serve a pinned copy |
|
|
174
|
+
themselves (web-react, Hikari).
|
|
175
|
+
|
|
176
|
+
| Import | What |
|
|
177
|
+
| --------------------------------- | ----------------------------------------------------------- |
|
|
178
|
+
| `@incorta/nexus-chat` | the connector (`mountNexusChat`, `loadNexusChat`) and types |
|
|
179
|
+
| `@incorta/nexus-chat/react` | `<NexusChat />` for React 18 and 19 apps |
|
|
180
|
+
| `@incorta/nexus-chat/contract` | the host contract's values (`NEXUS_CHAT_CONTRACT_VERSION`…) |
|
|
181
|
+
| `@incorta/nexus-chat/conformance` | checks a host bridge against the contract |
|
|
182
|
+
| `@incorta/nexus-chat/host` | host helpers (`createElementRootAdapter`…) |
|
|
93
183
|
|
|
94
184
|
## Versions
|
|
95
185
|
|
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,24 +255,49 @@ 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
|
-
await checks.run('chat.triggers', 'each trigger is @ or
|
|
270
|
+
await checks.run('chat.triggers', 'each trigger is a bare @ or #, or one with a search', () => {
|
|
269
271
|
const chars = (chat?.triggers ?? []).map((trigger) => {
|
|
272
|
+
if (typeof trigger === 'string') {
|
|
273
|
+
ensure(TRIGGER_CHARS.includes(trigger), `unknown trigger ${JSON.stringify(trigger)}; a bare string is one of: ${TRIGGER_CHARS.join(', ')}.`);
|
|
274
|
+
ensure(connection?.incortaApi?.type === 'bff', `the bare ${trigger} trigger needs a connection.incortaApi of type 'bff'.`);
|
|
275
|
+
return trigger;
|
|
276
|
+
}
|
|
270
277
|
ensure(TRIGGER_CHARS.includes(trigger.char), `unknown trigger ${JSON.stringify(trigger.char)}.`);
|
|
271
278
|
ensure(typeof trigger.search === 'function', `the ${trigger.char} trigger has no search().`);
|
|
272
279
|
return trigger.char;
|
|
273
280
|
});
|
|
274
281
|
ensure(new Set(chars).size === chars.length, 'a trigger is listed twice.');
|
|
275
282
|
});
|
|
283
|
+
const sessionLink = chat?.sessionLink;
|
|
284
|
+
if (!sessionLink) {
|
|
285
|
+
checks.skip('chat.sessionLink', 'links reopen a conversation', 'No session link declared.');
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
await checks.run('chat.sessionLink', 'links reopen a conversation', () => {
|
|
289
|
+
const link = sessionLink('contract-check-session');
|
|
290
|
+
let absolute = false;
|
|
291
|
+
try {
|
|
292
|
+
absolute = typeof link === 'string' && /^https?:$/.test(new URL(link).protocol);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// A relative path throws: the link must carry the host's origin.
|
|
296
|
+
}
|
|
297
|
+
ensure(absolute, `chat.sessionLink must return an absolute URL; received ${JSON.stringify(link)}.`);
|
|
298
|
+
ensure(link.includes('contract-check-session'), 'chat.sessionLink must carry the conversation id.');
|
|
299
|
+
});
|
|
300
|
+
}
|
|
276
301
|
await checks.run('actions', 'each action has can() and run()', () => {
|
|
277
302
|
Object.entries(actions ?? {}).forEach(([name, action]) => {
|
|
278
303
|
if (action === undefined)
|
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;
|
|
@@ -96,15 +133,18 @@ export interface ValueMatch {
|
|
|
96
133
|
dataType?: string;
|
|
97
134
|
complete?: boolean;
|
|
98
135
|
}
|
|
136
|
+
/** The composer trigger characters the chat can serve itself. */
|
|
137
|
+
export declare const BUILT_IN_TRIGGERS: readonly ["@", "#"];
|
|
138
|
+
export type BuiltInTrigger = (typeof BUILT_IN_TRIGGERS)[number];
|
|
99
139
|
/**
|
|
100
|
-
* A composer trigger
|
|
101
|
-
* view (`viewId` is that context's `id`); with none or several
|
|
102
|
-
* character stays plain text.
|
|
140
|
+
* A composer trigger the host serves. Both search inside the one attached
|
|
141
|
+
* business view (`viewId` is that context's `id`); with none or several
|
|
142
|
+
* attached the character stays plain text.
|
|
103
143
|
*
|
|
104
144
|
* - `@` mentions a column. Asked once per view; the chat filters as you type.
|
|
105
145
|
* - `#` searches the view's values for the typed text.
|
|
106
146
|
*/
|
|
107
|
-
export type
|
|
147
|
+
export type TriggerSource = {
|
|
108
148
|
char: '@';
|
|
109
149
|
search(request: {
|
|
110
150
|
viewId: string;
|
|
@@ -117,6 +157,21 @@ export type TriggerSpec = {
|
|
|
117
157
|
limit: number;
|
|
118
158
|
}, signal: AbortSignal): Promise<readonly ValueMatch[]>;
|
|
119
159
|
};
|
|
160
|
+
/**
|
|
161
|
+
* One composer trigger; `chat.triggers` lists them.
|
|
162
|
+
*
|
|
163
|
+
* - A bare `'@'` or `'#'`: the chat serves it itself through a BFF
|
|
164
|
+
* `connection.incortaApi`, across every attached business view and physical
|
|
165
|
+
* table. The user can narrow to one view, then one column, and `#` searches
|
|
166
|
+
* text columns only. Without a BFF `incortaApi` the character is left out
|
|
167
|
+
* with a console warning.
|
|
168
|
+
* - A `TriggerSource`: the host serves it, for one attached view.
|
|
169
|
+
*
|
|
170
|
+
* List each character once. Migrating: code that read `trigger.char` must
|
|
171
|
+
* narrow with `typeof trigger === 'string'` first. Pass bare characters only
|
|
172
|
+
* to a chat that has them (this release or later): an older chat ignores them.
|
|
173
|
+
*/
|
|
174
|
+
export type TriggerSpec = BuiltInTrigger | TriggerSource;
|
|
120
175
|
/** Switches for things the chat builds itself. Each one defaults to `true`. */
|
|
121
176
|
export interface NexusChatFeatures {
|
|
122
177
|
/**
|
|
@@ -237,6 +292,13 @@ export interface NexusChatChat {
|
|
|
237
292
|
beforeSend?(request: {
|
|
238
293
|
prompt: string;
|
|
239
294
|
}): SendAddition | Promise<SendAddition>;
|
|
295
|
+
/**
|
|
296
|
+
* The absolute URL that reopens a saved conversation in this host (the
|
|
297
|
+
* instance root plus the conversation id, for example
|
|
298
|
+
* `https://host/incorta/?chat-session-id=<id>`, in the page query before any hash route, which in-app redirects never drop). The host reads that URL
|
|
299
|
+
* back and calls `handle.openSession`. Absent: no Copy link.
|
|
300
|
+
*/
|
|
301
|
+
sessionLink?(sessionId: string): string;
|
|
240
302
|
}
|
|
241
303
|
export type OpenIntent = {
|
|
242
304
|
type: 'open-dashboard';
|
|
@@ -346,16 +408,33 @@ export interface NexusChatOptions {
|
|
|
346
408
|
* The host sizes the mount and owns its window (a page-covering overlay, a
|
|
347
409
|
* docked side panel) and draws its frame. Present: the chat hides its own
|
|
348
410
|
* fullscreen toggle and its edge border, and shows a dock toggle while
|
|
349
|
-
* `canDock
|
|
350
|
-
* and
|
|
411
|
+
* `canDock` (or, with `mode`, `modes` and `setMode`, one button per other
|
|
412
|
+
* mode), and a close button. Absent: the chat's own fullscreen toggle, and
|
|
413
|
+
* no close button.
|
|
351
414
|
*/
|
|
352
415
|
window?: NexusChatWindow;
|
|
353
416
|
}
|
|
417
|
+
/** The windows a host can show the chat in. */
|
|
418
|
+
export type NexusChatWindowMode = 'floating' | 'docked' | 'fullscreen';
|
|
354
419
|
export interface NexusChatWindowState {
|
|
355
420
|
/** The mount is the docked side panel (the toggle shows pressed). */
|
|
356
421
|
docked: boolean;
|
|
357
422
|
/** The host page allows docking now; `false` hides the dock toggle. */
|
|
358
423
|
canDock: boolean;
|
|
424
|
+
/**
|
|
425
|
+
* The window shown now. With `modes` and `setMode`, the header offers every
|
|
426
|
+
* mode in `modes` but this one (float, dock, full screen, in that order)
|
|
427
|
+
* instead of the dock toggle.
|
|
428
|
+
*/
|
|
429
|
+
mode?: NexusChatWindowMode;
|
|
430
|
+
/** The modes the host can switch to now, the current one included. */
|
|
431
|
+
modes?: readonly NexusChatWindowMode[];
|
|
432
|
+
/**
|
|
433
|
+
* The window is out of sight but the chat stays mounted (minimized, so the
|
|
434
|
+
* conversation stays). Anything that should not go on unseen stops: a
|
|
435
|
+
* dictation in progress ends and releases the microphone. Omitted means shown.
|
|
436
|
+
*/
|
|
437
|
+
hidden?: boolean;
|
|
359
438
|
}
|
|
360
439
|
export interface NexusChatWindow {
|
|
361
440
|
/** May return a fresh object on every call: the chat compares the fields. */
|
|
@@ -363,6 +442,8 @@ export interface NexusChatWindow {
|
|
|
363
442
|
/** Call `onChange` whenever `get` may return something different. */
|
|
364
443
|
subscribe(onChange: () => void): () => void;
|
|
365
444
|
setDocked(docked: boolean): void;
|
|
445
|
+
/** Shows the chat in another of `modes`; the conversation and draft stay. */
|
|
446
|
+
setMode?(mode: NexusChatWindowMode): void;
|
|
366
447
|
close(): void;
|
|
367
448
|
}
|
|
368
449
|
/** What `mount` returns: the host's handle on one running chat. */
|
|
@@ -382,6 +463,13 @@ export interface NexusChatHandle {
|
|
|
382
463
|
* gates: the agent must support the kind; `false` when refused).
|
|
383
464
|
*/
|
|
384
465
|
attach(kind: Exclude<ContextKind, 'file'>, item: ContextItem): boolean;
|
|
466
|
+
/**
|
|
467
|
+
* Opens a saved conversation by its id (a `chat.sessionLink` the host read
|
|
468
|
+
* back from its URL). A conversation with no messages, or one the backend
|
|
469
|
+
* refuses (deleted, someone else's), stays closed and the chat says so.
|
|
470
|
+
* While an unsent question is still being recovered nothing opens.
|
|
471
|
+
*/
|
|
472
|
+
openSession(sessionId: string): void;
|
|
385
473
|
}
|
|
386
474
|
/** The federated entry point `nexusChat/mount`. */
|
|
387
475
|
export type NexusChatMount = (element: HTMLElement, options: NexusChatOptions) => NexusChatHandle;
|
package/contract/index.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
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
|
+
];
|
|
14
|
+
/** The composer trigger characters the chat can serve itself. */
|
|
15
|
+
export const BUILT_IN_TRIGGERS = ['@', '#'];
|
|
2
16
|
/**
|
|
3
17
|
* Reads the namespaced-kind convention. Returns null for bare legacy kinds.
|
|
4
18
|
* Framework-free on purpose: hosts use this inside `canRender` to route by
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
HostTheme,
|
|
3
|
+
NexusChatHandle,
|
|
4
|
+
NexusChatMount,
|
|
5
|
+
NexusChatOptions,
|
|
6
|
+
} from './contract/index.js';
|
|
2
7
|
|
|
3
8
|
export type * from './contract/index.js';
|
|
4
9
|
|
|
@@ -6,8 +11,8 @@ export interface NexusChatSource {
|
|
|
6
11
|
/** IncortaCopilot's root as this page reaches it; the chat loads from `<copilotUrl>/nexus-chat/`. */
|
|
7
12
|
copilotUrl?: string;
|
|
8
13
|
/**
|
|
9
|
-
* Where the chat files are served instead (pinned):
|
|
10
|
-
*
|
|
14
|
+
* Where the chat files are served instead (pinned): any URL that serves a
|
|
15
|
+
* release's extracted `nexus-chat-dist.tar.gz`. Absent: from Copilot.
|
|
11
16
|
*/
|
|
12
17
|
assetsUrl?: string;
|
|
13
18
|
/** Deadline for loading the chat, retry included. Default 20 000 ms. */
|
|
@@ -24,8 +29,23 @@ export const CONTRACT_VERSION: 2;
|
|
|
24
29
|
/** Loads the chat once per source (one retry, one deadline) and resolves to its `mount`. */
|
|
25
30
|
export function loadNexusChat(source: NexusChatSource): Promise<NexusChatMount>;
|
|
26
31
|
|
|
32
|
+
/** Forgets a loaded chat, so the next load fetches it again (after it failed while running). */
|
|
33
|
+
export function forgetNexusChat(source: NexusChatSource): void;
|
|
34
|
+
|
|
35
|
+
/** The version the last loaded chat reports (for error reports); undefined before a load. */
|
|
36
|
+
export function loadedNexusChatVersion(): string | undefined;
|
|
37
|
+
|
|
27
38
|
/** Loads the chat and mounts it into `element`. */
|
|
28
39
|
export function mountNexusChat(
|
|
29
40
|
element: HTMLElement,
|
|
30
41
|
options: MountNexusChatOptions,
|
|
31
42
|
): Promise<NexusChatHandle>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @internal The skeleton `mountNexusChat` and `<NexusChat>` draw over a box
|
|
46
|
+
* while the chat loads. Not part of the package's API.
|
|
47
|
+
*/
|
|
48
|
+
export function showNexusChatSkeleton(
|
|
49
|
+
box: HTMLElement,
|
|
50
|
+
theme?: HostTheme,
|
|
51
|
+
): { setTheme(theme?: HostTheme): void; done(): void; remove(): void };
|