@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.
Files changed (78) hide show
  1. package/README.md +134 -44
  2. package/contract/conformance.js +30 -5
  3. package/contract/index.d.ts +96 -8
  4. package/contract/index.js +14 -0
  5. package/index.d.ts +23 -3
  6. package/index.js +238 -14
  7. package/package.json +19 -4
  8. package/react.d.ts +77 -17
  9. package/react.js +240 -35
  10. package/window.js +383 -0
  11. package/remote/assets/AgentOptionControl-BnqfB5W3.js +0 -1
  12. package/remote/assets/CsvPreview-DD2VELWa.js +0 -10
  13. package/remote/assets/ErrorCard-Bu_0QAJ9.js +0 -1
  14. package/remote/assets/HighlightedCode-DpxGYHfF.js +0 -1
  15. package/remote/assets/HistorySearchDialog-q4cvx5jy.js +0 -41
  16. package/remote/assets/HostBridgeProvider-BV-UGU3Q.js +0 -1
  17. package/remote/assets/HtmlArtifactRenderer-EO_cwaEm.js +0 -1
  18. package/remote/assets/I18nContext-BlQKoDmv.js +0 -24
  19. package/remote/assets/PreviewState-VcIZyCXJ.js +0 -1
  20. package/remote/assets/SourceCitations-DvApeC9N.js +0 -1
  21. package/remote/assets/SourcesArtifactRenderer-BqnIOctm.js +0 -1
  22. package/remote/assets/TableArtifactRenderer-BLZLQMxU.js +0 -2
  23. package/remote/assets/TiptapComposerInput-CmxEiBlE.js +0 -132
  24. package/remote/assets/UnsupportedArtifact-CeOCu_J6.js +0 -1
  25. package/remote/assets/_virtual_mf-localSharedImportMap___mfe_internal__nexusChat__mf_owner__1-CObPUVU6.js +0 -1
  26. package/remote/assets/ar-BV81JAir.js +0 -1
  27. package/remote/assets/button-CO0tH6F2.js +0 -1
  28. package/remote/assets/client-Bv7EbCrM.js +0 -2
  29. package/remote/assets/composite-giRhJC3d.js +0 -1
  30. package/remote/assets/conformance-entry-uotihsU2.js +0 -2
  31. package/remote/assets/createLucideIcon-D1A0JYKN.js +0 -1
  32. package/remote/assets/de-BXJlhmC2.js +0 -1
  33. package/remote/assets/defineProperty-BbfpZ9Tg.js +0 -1
  34. package/remote/assets/dist-MaNcI-Om.js +0 -4
  35. package/remote/assets/errorReporter-hCgThzof.js +0 -1
  36. package/remote/assets/es-nnw68YFM.js +0 -1
  37. package/remote/assets/federation-entry-B8pojL73.css +0 -2
  38. package/remote/assets/federation-entry-BA9UpYiM.js +0 -65
  39. package/remote/assets/floating-ui.dom-B99QSfPH.js +0 -1
  40. package/remote/assets/floating-ui.utils.dom-ObnfY4F6.js +0 -1
  41. package/remote/assets/fr-CJIAbbzn.js +0 -1
  42. package/remote/assets/highlighter-C37NKSjx.js +0 -19
  43. package/remote/assets/hostInit-YMtJ2r-g.js +0 -1
  44. package/remote/assets/it-D3TiEKfy.js +0 -1
  45. package/remote/assets/ja-ZB0L4Lsc.js +0 -1
  46. package/remote/assets/jsx-runtime-DE3RlOCf.js +0 -1
  47. package/remote/assets/lib-CjT9YRPC.js +0 -1
  48. package/remote/assets/modelProviderIcons-fiyx4SaW.js +0 -1
  49. package/remote/assets/react-dom-FxOPXJ08.js +0 -1
  50. package/remote/assets/rehype-raw-CNOzW4vu.js +0 -2
  51. package/remote/assets/roboto-latin-400-italic-CSuqwwKr.woff2 +0 -0
  52. package/remote/assets/roboto-latin-400-normal-BqEyEoaF.woff2 +0 -0
  53. package/remote/assets/roboto-latin-500-italic-DOe3GFcv.woff2 +0 -0
  54. package/remote/assets/roboto-latin-500-normal-7RbcRiD8.woff2 +0 -0
  55. package/remote/assets/roboto-latin-700-italic-Cb9agdGy.woff2 +0 -0
  56. package/remote/assets/roboto-latin-700-normal-BZpUvMxY.woff2 +0 -0
  57. package/remote/assets/roboto-latin-ext-400-italic-BilqUbDw.woff2 +0 -0
  58. package/remote/assets/roboto-latin-ext-400-normal-C3tdtHj3.woff2 +0 -0
  59. package/remote/assets/roboto-latin-ext-500-italic-B5teBpxj.woff2 +0 -0
  60. package/remote/assets/roboto-latin-ext-500-normal-pMCM9Ixg.woff2 +0 -0
  61. package/remote/assets/roboto-latin-ext-700-italic-CBdFfgzf.woff2 +0 -0
  62. package/remote/assets/roboto-latin-ext-700-normal-DSBUz0N1.woff2 +0 -0
  63. package/remote/assets/rolldown-runtime-hePW80VL.js +0 -1
  64. package/remote/assets/sessionTitle-C9LhAN0r.js +0 -1
  65. package/remote/assets/spinner-eqRx6Fq-.js +0 -1
  66. package/remote/assets/sqlFormatting-BjJIfUN_.js +0 -7
  67. package/remote/assets/src-CIHHGjAO.js +0 -1
  68. package/remote/assets/store-FaSYSP4_.js +0 -1
  69. package/remote/assets/useBaseUiId-Dpqq0brK.js +0 -1
  70. package/remote/assets/usePositioner-BhCjFmeW.js +0 -1
  71. package/remote/assets/virtualExposes-C0oWo8z2.js +0 -1
  72. package/remote/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__nexusChat__remoteEntry_js-5uUvXWWH.js +0 -1
  73. package/remote/assets/virtual_mf-exposes-ssr___mfe_internal__nexusChat__remoteEntry_js-WBZ0Muv9.js +0 -2
  74. package/remote/assets/virtual_mf-exposes___mfe_internal__nexusChat__remoteEntry_js-BGZ2nkwm.js +0 -2
  75. package/remote/assets/vite-preload-helper-B7qeedMF.js +0 -1
  76. package/remote/assets/zh-De2Ilp0M.js +0 -1
  77. package/remote/remoteEntry.js +0 -1
  78. package/remote/remoteEntry.ssr.js +0 -1
package/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: `<NexusChat />` for React,
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. Importing the connector and the component adds about 3 KB to an app; `remote/` stays on
9
- disk unless the app serves it.
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={{ client: 'general', copilotUrl: '/api/ai', credentials: 'same-origin' }}
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
- The chat fills the component's box. `theme`, `locale` (default: the browser's), `direction`,
25
- `tenantId`, `user` and `window` update the running chat; `connection`, `chat`, `actions`, `events`,
26
- `instanceId`, `assetsUrl` and `timeoutMs` are read once at mount. `window` (`docked`, `canDock`,
27
- `setDocked`, `close`) hands the chat's window to the app: the chat shows a dock toggle and a close
28
- button. `onReady` receives the handle (`ask`, `attach`).
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. Mount in the browser
48
- only, never during SSR.
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); a failed load is forgotten so the next
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 | Options | UI version | App setup |
57
- | ------------------------- | ----------------------------------------------------------------------------- | ----------------------------- | ----------------------------------------------- |
58
- | Always updated (default) | `connection.copilotUrl` only | what that Copilot runs | none: Copilot serves `<copilotUrl>/nexus-chat/` |
59
- | Pinned, served by the app | `assetsUrl: '/nexus-chat/'` | the installed package version | serve this package's `remote/` at that path |
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 static app copies it instead:
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) and apps that serve a pinned copy.
84
-
85
- | Import | What |
86
- | ------------------------------------------ | ----------------------------------------------------------- |
87
- | `@incorta/nexus-chat` | the connector (`mountNexusChat`, `loadNexusChat`) and types |
88
- | `@incorta/nexus-chat/react` | `<NexusChat />` for React 18 and 19 apps |
89
- | `@incorta/nexus-chat/contract` | the host contract's values (`NEXUS_CHAT_CONTRACT_VERSION`…) |
90
- | `@incorta/nexus-chat/conformance` | checks a host bridge against the contract |
91
- | `@incorta/nexus-chat/host` | host helpers (`createElementRootAdapter`…) |
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
 
@@ -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 "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
- await checks.run('chat.triggers', 'each trigger is @ or # with a search', () => {
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)
@@ -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;
@@ -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 character. Both search inside the one attached business
101
- * view (`viewId` is that context's `id`); with none or several attached the
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 TriggerSpec = {
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`, and a close button. Absent: the chat's own fullscreen toggle,
350
- * and no close button.
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 { NexusChatHandle, NexusChatMount, NexusChatOptions } from './contract/index.js';
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): the app's own copy of
10
- * this package's `remote/` folder, or a CDN URL. Absent: from Copilot.
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 };