@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/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,18 +20,91 @@ 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;
|
|
26
|
+
|
|
27
|
+
// A loop, not `/\/+$/`: that regex backtracks quadratically on many slashes.
|
|
28
|
+
function withoutTrailingSlashes(url) {
|
|
29
|
+
let end = url.length;
|
|
30
|
+
while (url[end - 1] === '/') end -= 1;
|
|
31
|
+
return url.slice(0, end);
|
|
32
|
+
}
|
|
24
33
|
|
|
25
34
|
function entryUrl({ copilotUrl, assetsUrl }) {
|
|
26
|
-
const base = assetsUrl ?? (copilotUrl && `${copilotUrl
|
|
35
|
+
const base = assetsUrl ?? (copilotUrl && `${withoutTrailingSlashes(copilotUrl)}/nexus-chat/`);
|
|
27
36
|
if (!base) throw new Error('NexusChat needs a copilotUrl or an assetsUrl.');
|
|
28
37
|
return new URL('remoteEntry.js', new URL(base, window.location.href)).href;
|
|
29
38
|
}
|
|
30
39
|
|
|
40
|
+
// A failed CSS chunk rejects the load, and Vite's preload never asks for a
|
|
41
|
+
// sheet it has seen again. Remember the chat's failed sheets so the next
|
|
42
|
+
// attempt requests them anew. A sheet failing again fails that attempt but is
|
|
43
|
+
// forgotten, so one a deploy removed blocks a single attempt, not every one.
|
|
44
|
+
function captureFailedStylesheets(base) {
|
|
45
|
+
if (captureFailedStylesheets.installed) return;
|
|
46
|
+
captureFailedStylesheets.installed = true;
|
|
47
|
+
window.addEventListener(
|
|
48
|
+
'error',
|
|
49
|
+
({ target }) => {
|
|
50
|
+
if (!(target instanceof HTMLLinkElement) || target.rel !== 'stylesheet') return;
|
|
51
|
+
if (target.dataset.nexusChatRetry === undefined && target.href.startsWith(base)) {
|
|
52
|
+
failedStylesheets.add(target.href);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
true,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function reloadFailedStylesheets() {
|
|
60
|
+
const reloads = [...failedStylesheets].map((href) => {
|
|
61
|
+
failedStylesheets.delete(href);
|
|
62
|
+
document.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
|
|
63
|
+
if (link.href === href) link.remove();
|
|
64
|
+
});
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const link = Object.assign(document.createElement('link'), { rel: 'stylesheet', href });
|
|
67
|
+
link.dataset.nexusChatRetry = '';
|
|
68
|
+
link.onload = resolve;
|
|
69
|
+
link.onerror = () => {
|
|
70
|
+
link.remove();
|
|
71
|
+
reject(new Error(`NexusChat stylesheet failed to load: ${href}`));
|
|
72
|
+
};
|
|
73
|
+
document.head.append(link);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
return Promise.all(reloads);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// A chat served by its Vite dev server ships React Fast Refresh code, which
|
|
80
|
+
// throws unless the page ran @vitejs/plugin-react's preamble. Only a local
|
|
81
|
+
// dev server can be one, so only there is it probed (GET; Vite ignores HEAD).
|
|
82
|
+
async function installReactRefreshPreamble(base) {
|
|
83
|
+
if (window.__vite_plugin_react_preamble_installed__) return;
|
|
84
|
+
if (!/^(localhost|127\.0\.0\.1|\[::1\])$|\.localhost$/.test(new URL(base).hostname)) return;
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL('@react-refresh', base).href;
|
|
87
|
+
const probe = await fetch(url, { signal: AbortSignal.timeout(3_000) });
|
|
88
|
+
if (!probe.ok || !probe.headers.get('content-type')?.includes('javascript')) return;
|
|
89
|
+
const runtime = await import(/* @vite-ignore */ /* webpackIgnore: true */ url);
|
|
90
|
+
runtime.default.injectIntoGlobalHook(window);
|
|
91
|
+
window.$RefreshReg$ = () => {};
|
|
92
|
+
window.$RefreshSig$ = () => (type) => type;
|
|
93
|
+
window.__vite_plugin_react_preamble_installed__ = true;
|
|
94
|
+
} catch {
|
|
95
|
+
// Not a dev-served chat.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
31
99
|
async function importMount(entry) {
|
|
32
100
|
// The browser remembers a failed import by URL, so every attempt after the
|
|
33
101
|
// first asks for a new one, or it would never reach the network again.
|
|
34
102
|
const attempt = attempts.get(entry) ?? 0;
|
|
35
103
|
attempts.set(entry, attempt + 1);
|
|
104
|
+
const base = new URL('.', entry).href;
|
|
105
|
+
captureFailedStylesheets(base);
|
|
106
|
+
await installReactRefreshPreamble(base);
|
|
107
|
+
if (attempt) await reloadFailedStylesheets();
|
|
36
108
|
const url = new URL(entry);
|
|
37
109
|
if (attempt) url.searchParams.set('nexus-chat-attempt', String(attempt));
|
|
38
110
|
const remote = await import(/* @vite-ignore */ /* webpackIgnore: true */ url.href);
|
|
@@ -42,9 +114,43 @@ async function importMount(entry) {
|
|
|
42
114
|
if (typeof module?.default !== 'function') {
|
|
43
115
|
throw new Error('NexusChat remote did not expose a mount function.');
|
|
44
116
|
}
|
|
117
|
+
loadedVersion =
|
|
118
|
+
typeof module.NEXUS_CHAT_VERSION === 'string' ? module.NEXUS_CHAT_VERSION : undefined;
|
|
45
119
|
return module.default;
|
|
46
120
|
}
|
|
47
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Forgets a loaded chat, so the next load fetches it again: after it failed
|
|
124
|
+
* while running (a redeploy can leave it asking for files that are gone).
|
|
125
|
+
*/
|
|
126
|
+
export function forgetNexusChat(source) {
|
|
127
|
+
// A source with no URL was never loaded.
|
|
128
|
+
if (source.copilotUrl || source.assetsUrl) loads.delete(entryUrl(source));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The version the last loaded chat reports (for error reports); undefined before a load. */
|
|
132
|
+
export function loadedNexusChatVersion() {
|
|
133
|
+
return loadedVersion;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Rejects after `timeoutMs`, counted while the page can be seen: a hidden tab
|
|
138
|
+
* runs its timers and requests slowly (Chrome throttles background tabs), so
|
|
139
|
+
* a chat loading there gets a fresh deadline once the tab shows. Internal to
|
|
140
|
+
* this package.
|
|
141
|
+
*/
|
|
142
|
+
export function loadDeadline(timeoutMs) {
|
|
143
|
+
return new Promise((_, reject) => {
|
|
144
|
+
const expire = () => {
|
|
145
|
+
if (!document.hidden) return reject(new Error('Timed out loading NexusChat.'));
|
|
146
|
+
document.addEventListener('visibilitychange', () => setTimeout(expire, timeoutMs), {
|
|
147
|
+
once: true,
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
setTimeout(expire, timeoutMs);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
48
154
|
/**
|
|
49
155
|
* Loads the chat once per source and resolves to its `mount` function. One
|
|
50
156
|
* retry, since a single failure (a 502 during a rolling restart) is not an
|
|
@@ -62,9 +168,7 @@ export function loadNexusChat(options) {
|
|
|
62
168
|
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
63
169
|
return importMount(entry);
|
|
64
170
|
}),
|
|
65
|
-
|
|
66
|
-
setTimeout(() => reject(new Error('Timed out loading NexusChat.')), timeoutMs),
|
|
67
|
-
),
|
|
171
|
+
loadDeadline(timeoutMs),
|
|
68
172
|
]);
|
|
69
173
|
load.catch(() => loads.delete(entry));
|
|
70
174
|
loads.set(entry, load);
|
|
@@ -75,13 +179,133 @@ export function loadNexusChat(options) {
|
|
|
75
179
|
/**
|
|
76
180
|
* Loads the chat (from `assetsUrl`, else `connection.copilotUrl`) and mounts
|
|
77
181
|
* it into `element` with the host contract options. The chat fills the
|
|
78
|
-
* element, so the element's box decides the presentation.
|
|
182
|
+
* element, so the element's box decides the presentation. Until the chat is
|
|
183
|
+
* up the skeleton holds the element; it leaves at once if the chat cannot load.
|
|
79
184
|
*/
|
|
80
185
|
export async function mountNexusChat(element, { assetsUrl, timeoutMs, ...options }) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
186
|
+
const skeleton = showNexusChatSkeleton(element, options.environment?.get().theme);
|
|
187
|
+
let handle;
|
|
188
|
+
try {
|
|
189
|
+
const mount = await loadNexusChat({
|
|
190
|
+
copilotUrl: options.connection?.copilotUrl,
|
|
191
|
+
assetsUrl,
|
|
192
|
+
timeoutMs,
|
|
193
|
+
});
|
|
194
|
+
handle = mount(element, { version: CONTRACT_VERSION, ...options });
|
|
195
|
+
} catch (error) {
|
|
196
|
+
skeleton.remove();
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
skeleton.done();
|
|
200
|
+
return {
|
|
201
|
+
...handle,
|
|
202
|
+
unmount() {
|
|
203
|
+
skeleton.remove();
|
|
204
|
+
handle.unmount();
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---- the loading face every app shows until the chat is up (<NexusChat> draws it too)
|
|
210
|
+
|
|
211
|
+
/** Fast loads show no skeleton; one that shows stays long enough to read, then cross-fades. */
|
|
212
|
+
const SKELETON_DELAY_MS = 300;
|
|
213
|
+
const SKELETON_MIN_MS = 400;
|
|
214
|
+
const SKELETON_FADE_MS = 150;
|
|
215
|
+
|
|
216
|
+
/** The chat's own ground (so the swap does not flash), its line fill, its window edge. */
|
|
217
|
+
export const PALETTE = {
|
|
218
|
+
light: { ground: '#fcfdfe', fill: '#efeff2', line: '#e4e4e7' },
|
|
219
|
+
dark: { ground: '#151515', fill: '#242426', line: '#2c2c2e' },
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export const reducedMotion = () => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
223
|
+
|
|
224
|
+
/** The Incorta Intelligence mark. */
|
|
225
|
+
export const ICON = `data:image/svg+xml,${encodeURIComponent(
|
|
226
|
+
"<svg width='107' height='120' viewBox='0 0 107 120' fill='none' xmlns='http://www.w3.org/2000/svg'><g clip-path='url(#clip0_108_873)'><path d='M84 35.4C89.8 37.6 95.2 41.2 99.8 45.9C102.4 48.6 104.6 51.5 106.5 54.6L106.7 35.4C106.7 32 104.9 28.8 102 27L63.1 3.9C66.2 5.8 69.1 8.1 71.7 10.9C78.5 17.8 82.6 26.4 84 35.4Z' fill='url(#paint0_linear_108_873)'/><path d='M22.9 84.8C17.1 82.5 11.7 79 7.1 74.3C4.4 71.5 2.1 68.4 0.2 65.2L0 84.6C0 88.1 1.7 91.2 4.7 93L43.9 116.3C40.8 114.4 37.9 112.1 35.2 109.3C28.4 102.4 24.3 93.8 22.9 84.8Z' fill='url(#paint1_linear_108_873)'/><path d='M106.1 85.8L106.5 54.6C104.6 51.5 102.4 48.6 99.8 45.9C95.2 41.2 89.8 37.6 84 35.4C86.3 49.4 81.9 64.3 71 75C57.9 87.9 38.9 91.1 22.9 84.8C24.3 93.8 28.4 102.4 35.2 109.3C37.9 112.1 40.8 114.4 43.9 116.3L45.2 117.1L47.9 118.7C48.9 119.3 49.9 119.7 51 119.9C53.2 120.3 55.5 119.9 57.5 118.8L101.3 94.1C104.2 92.4 106.1 89.2 106.1 85.8V85.8Z' fill='url(#paint2_linear_108_873)'/><path d='M35.9 45.2C49 32.3 68 29.1 84 35.4C82.6 26.4 78.5 17.8 71.7 10.8C69.1 8.1 66.2 5.8 63.1 3.9L61.7 3.1L58.8 1.3C57.7 0.7 56.6 0.3 55.4 0.1C53.3 -0.2 51.1 0.2 49.2 1.2L5.4 26C2.4 27.6 0.6 30.8 0.6 34.2L0.2 65.2C2.1 68.4 4.4 71.5 7.1 74.3C11.7 79 17.1 82.5 22.9 84.8C20.7 70.7 25 55.9 35.9 45.2V45.2Z' fill='#00A098'/><path d='M22.9 84.8C23 85.3 23.1 85.8 23.2 86.3C23.2 86.5 23.2 86.7 23.3 86.8C23.3 87.2 23.4 87.5 23.5 87.9C23.5 88 23.6 88.2 23.6 88.4C23.7 88.8 23.8 89.1 23.9 89.4C23.9 89.6 24 89.8 24 90C24.1 90.4 24.2 90.8 24.4 91.1C24.4 91.4 24.5 91.6 24.6 91.9C24.7 92.2 24.8 92.5 24.9 92.8C25 93.1 25.1 93.3 25.2 93.6C25.3 93.8 25.4 94 25.5 94.2C25.6 94.5 25.7 94.8 25.8 95C25.9 95.2 25.9 95.4 26 95.6C26.2 95.9 26.3 96.2 26.4 96.5C26.5 96.6 26.5 96.7 26.5 96.7C26.9 97.4 27.2 98.1 27.6 98.8C27.6 98.9 27.6 98.9 27.7 99C27.9 99.3 28 99.7 28.2 100C28.3 100.1 28.4 100.2 28.4 100.3C28.6 100.7 28.8 101 29 101.3C29 101.4 29.1 101.5 29.1 101.6C29.8 102.7 30.6 103.8 31.4 104.9C31.8 104.9 92.7 102.9 96.2 42.5C96.6 36.7 96.4 30.2 95.5 23.2L63.1 3.9C66.2 5.8 69.1 8.1 71.7 10.9C78.5 17.8 82.6 26.4 84 35.4C86.3 49.4 81.9 64.3 71 75C57.9 87.9 38.9 91.1 22.9 84.8V84.8Z' fill='url(#paint3_linear_108_873)'/><path d='M84 35.4C84 34.9 83.9 34.4 83.8 33.8C83.7 33.7 83.7 33.5 83.7 33.4C83.6 33 83.5 32.7 83.4 32.3C83.4 32.1 83.3 31.9 83.3 31.7C83.2 31.4 83.1 31.1 83.1 30.8C83 30.6 83 30.4 82.9 30.2C82.8 29.8 82.7 29.4 82.6 29C82.5 28.8 82.4 28.5 82.3 28.3C82.2 28 82.1 27.7 82 27.4C81.9 27.1 81.8 26.9 81.7 26.6C81.6 26.4 81.5 26.2 81.5 25.9C81.4 25.7 81.2 25.4 81.1 25.1C81.1 24.9 81 24.7 80.9 24.6C80.8 24.3 80.6 24 80.5 23.7C80.5 23.6 80.4 23.5 80.4 23.4C80.1 22.7 79.7 22.1 79.4 21.4C79.3 21.3 79.3 21.2 79.2 21.2C79.1 20.8 78.9 20.5 78.7 20.2C78.6 20.1 78.6 20 78.5 19.9C78.3 19.5 78.1 19.2 77.9 18.9C77.9 18.8 77.8 18.7 77.8 18.6C77.1 17.5 76.3 16.4 75.5 15.3C75.1 15.3 14.2 17.3 10.7 77.6C10.4 83.5 10.6 90 11.4 97L43.8 116.2C40.7 114.3 37.8 112 35.2 109.3C28.4 102.4 24.3 93.8 22.9 84.8C20.7 70.8 25 55.9 35.9 45.2C49 32.3 68 29.1 84 35.4V35.4Z' fill='url(#paint4_linear_108_873)'/></g><defs><linearGradient id='paint0_linear_108_873' x1='104.6' y1='41.1' x2='52.2' y2='1.6' gradientUnits='userSpaceOnUse'><stop stop-color='#4854FE'/><stop offset='1' stop-color='#241C55'/></linearGradient><linearGradient id='paint1_linear_108_873' x1='0' y1='90.8' x2='43.9' y2='90.8' gradientUnits='userSpaceOnUse'><stop stop-color='#4854FE'/><stop offset='1' stop-color='#241C55'/></linearGradient><linearGradient id='paint2_linear_108_873' x1='31.5' y1='40.6' x2='92.7' y2='102.9' gradientUnits='userSpaceOnUse'><stop stop-color='white'/><stop offset='1' stop-color='#4854FE'/></linearGradient><linearGradient id='paint3_linear_108_873' x1='86.3' y1='19.1' x2='41.6' y2='98.7' gradientUnits='userSpaceOnUse'><stop stop-color='#4854FE' stop-opacity='0.93'/><stop offset='1' stop-color='#006EF0' stop-opacity='0'/></linearGradient><linearGradient id='paint4_linear_108_873' x1='19.9' y1='103.3' x2='61' y2='27.5' gradientUnits='userSpaceOnUse'><stop stop-color='#006EF0'/><stop offset='1' stop-color='#006EF0' stop-opacity='0'/></linearGradient><clipPath id='clip0_108_873'><rect width='106.7' height='120' fill='white'/></clipPath></defs></svg>",
|
|
227
|
+
)}`;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Draws the skeleton over `box`: the mark and two lines centred in it,
|
|
231
|
+
* pulsing together, revealed only once loading has taken SKELETON_DELAY_MS.
|
|
232
|
+
* It stays on top of whatever renders into `box` (the chat's React empties its
|
|
233
|
+
* element when it first paints) until `done` fades it out (after showing for
|
|
234
|
+
* SKELETON_MIN_MS) or `remove` drops it at once. Internal to this package.
|
|
235
|
+
*/
|
|
236
|
+
export function showNexusChatSkeleton(box, theme) {
|
|
237
|
+
const root = document.createElement('div');
|
|
238
|
+
root.setAttribute('role', 'status');
|
|
239
|
+
root.setAttribute('aria-label', 'Loading Incorta Intelligence');
|
|
240
|
+
root.style.cssText =
|
|
241
|
+
'position:absolute;inset:0;box-sizing:border-box;padding:24px;display:flex;align-items:center;justify-content:center;pointer-events:none';
|
|
242
|
+
const line = (width, height) =>
|
|
243
|
+
`<span style="display:block;width:${width};height:${height}px;border-radius:${height / 2}px"></span>`;
|
|
244
|
+
root.innerHTML = `<div style="width:100%;max-width:280px"><div style="display:flex;flex-direction:column;align-items:center;gap:20px"><img alt="" width="32" height="34"><div style="display:flex;flex-direction:column;align-items:center;gap:10px;width:100%">${line('100%', 14)}${line('64%', 10)}</div></div></div>`;
|
|
245
|
+
root.querySelector('img').src = ICON;
|
|
246
|
+
const setTheme = (next) => {
|
|
247
|
+
const palette = PALETTE[next === 'dark' ? 'dark' : 'light'];
|
|
248
|
+
root.style.background = palette.ground;
|
|
249
|
+
for (const span of root.querySelectorAll('span')) span.style.background = palette.fill;
|
|
250
|
+
};
|
|
251
|
+
setTheme(theme);
|
|
252
|
+
|
|
253
|
+
// An unpositioned box becomes the skeleton's containing block while it shows.
|
|
254
|
+
const position = box.style.position;
|
|
255
|
+
const unpositioned = getComputedStyle(box).position === 'static';
|
|
256
|
+
if (unpositioned) box.style.position = 'relative';
|
|
257
|
+
box.append(root);
|
|
258
|
+
const stay = new MutationObserver(() => root.parentNode === box || box.append(root));
|
|
259
|
+
stay.observe(box, { childList: true });
|
|
260
|
+
|
|
261
|
+
const shownAt = Date.now() + SKELETON_DELAY_MS;
|
|
262
|
+
const stack = root.firstChild;
|
|
263
|
+
const animations = [
|
|
264
|
+
stack.animate?.([{ opacity: 0 }, { opacity: 1 }], {
|
|
265
|
+
duration: SKELETON_FADE_MS,
|
|
266
|
+
delay: SKELETON_DELAY_MS,
|
|
267
|
+
fill: 'backwards',
|
|
268
|
+
}),
|
|
269
|
+
reducedMotion()
|
|
270
|
+
? undefined
|
|
271
|
+
: stack.firstChild.animate?.([{ opacity: 1 }, { opacity: 0.45 }, { opacity: 1 }], {
|
|
272
|
+
duration: 1600,
|
|
273
|
+
easing: 'ease-in-out',
|
|
274
|
+
iterations: Infinity,
|
|
275
|
+
}),
|
|
276
|
+
];
|
|
277
|
+
let timer;
|
|
278
|
+
let gone = false;
|
|
279
|
+
const remove = () => {
|
|
280
|
+
if (gone) return;
|
|
281
|
+
gone = true;
|
|
282
|
+
clearTimeout(timer);
|
|
283
|
+
stay.disconnect();
|
|
284
|
+
animations.forEach((animation) => animation?.cancel());
|
|
285
|
+
root.remove();
|
|
286
|
+
if (unpositioned) box.style.position = position;
|
|
287
|
+
};
|
|
288
|
+
return {
|
|
289
|
+
setTheme,
|
|
290
|
+
remove,
|
|
291
|
+
done() {
|
|
292
|
+
if (gone || timer !== undefined) return;
|
|
293
|
+
const sinceShown = Date.now() - shownAt;
|
|
294
|
+
// Never shown (a fast load): gone at once, before its reveal can start.
|
|
295
|
+
if (sinceShown < 0) return remove();
|
|
296
|
+
// Shown: for at least SKELETON_MIN_MS, then it fades.
|
|
297
|
+
timer = setTimeout(
|
|
298
|
+
() => {
|
|
299
|
+
const fade = root.animate?.([{ opacity: 1 }, { opacity: 0 }], {
|
|
300
|
+
duration: SKELETON_FADE_MS,
|
|
301
|
+
fill: 'forwards',
|
|
302
|
+
});
|
|
303
|
+
if (!fade) return remove();
|
|
304
|
+
animations.push(fade);
|
|
305
|
+
fade.onfinish = remove;
|
|
306
|
+
},
|
|
307
|
+
Math.max(0, SKELETON_MIN_MS - sinceShown),
|
|
308
|
+
);
|
|
309
|
+
},
|
|
310
|
+
};
|
|
87
311
|
}
|
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.20",
|
|
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,16 +31,31 @@
|
|
|
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
|
},
|
|
36
|
+
"typesVersions": {
|
|
37
|
+
"*": {
|
|
38
|
+
"react": [
|
|
39
|
+
"./react.d.ts"
|
|
40
|
+
],
|
|
41
|
+
"contract": [
|
|
42
|
+
"./contract/index.d.ts"
|
|
43
|
+
],
|
|
44
|
+
"conformance": [
|
|
45
|
+
"./contract/conformance.d.ts"
|
|
46
|
+
],
|
|
47
|
+
"host": [
|
|
48
|
+
"./contract/host.d.ts"
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
},
|
|
37
52
|
"files": [
|
|
38
53
|
"index.js",
|
|
39
54
|
"index.d.ts",
|
|
40
55
|
"react.js",
|
|
56
|
+
"window.js",
|
|
41
57
|
"react.d.ts",
|
|
42
58
|
"contract",
|
|
43
|
-
"remote",
|
|
44
59
|
"README.md"
|
|
45
60
|
],
|
|
46
61
|
"publishConfig": {
|
package/react.d.ts
CHANGED
|
@@ -5,38 +5,98 @@ import type {
|
|
|
5
5
|
HostTheme,
|
|
6
6
|
HostUser,
|
|
7
7
|
MountNexusChatOptions,
|
|
8
|
+
NexusChatEnvironment,
|
|
8
9
|
NexusChatHandle,
|
|
10
|
+
NexusChatWindow,
|
|
11
|
+
NexusChatWindowMode,
|
|
9
12
|
} from './index.js';
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
interface DirectChatProps extends Omit<MountNexusChatOptions, 'environment' | 'window'> {
|
|
15
|
+
embedUrl?: undefined;
|
|
16
|
+
/** Required unless `environment` is given. */
|
|
17
|
+
theme?: HostTheme;
|
|
13
18
|
/** Default: the browser's language. */
|
|
14
19
|
locale?: string;
|
|
15
20
|
/** Absent: follows `locale`. */
|
|
16
21
|
direction?: HostDirection;
|
|
17
22
|
tenantId?: string;
|
|
18
23
|
user?: HostUser;
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
window?: {
|
|
24
|
-
docked: boolean;
|
|
25
|
-
canDock: boolean;
|
|
26
|
-
setDocked(docked: boolean): void;
|
|
27
|
-
close(): void;
|
|
28
|
-
};
|
|
29
|
-
/** The chat is mounted: `ask` and `attach` are available. */
|
|
30
|
-
onReady?(chat: NexusChatHandle): void;
|
|
24
|
+
/** The app's own contract environment, used as is instead of the props above. */
|
|
25
|
+
environment?: NexusChatEnvironment;
|
|
26
|
+
/** The chat is mounted (`ask` and `attach` are available); `null` once it unmounts. */
|
|
27
|
+
onReady?(chat: NexusChatHandle | null): void;
|
|
31
28
|
/** The chat could not load (after its retry and deadline). */
|
|
32
29
|
onLoadError?(error: Error): void;
|
|
33
30
|
className?: string;
|
|
34
31
|
style?: CSSProperties;
|
|
35
32
|
}
|
|
36
33
|
|
|
34
|
+
/** The chat mounted into this page, sharing its session. It fills its box. */
|
|
35
|
+
export interface DirectNexusChatProps extends DirectChatProps {
|
|
36
|
+
/**
|
|
37
|
+
* The app owns the chat's window (a docked panel, an overlay): the chat
|
|
38
|
+
* shows a dock toggle while `canDock` (or, with `mode`, `modes` and
|
|
39
|
+
* `setMode`, a button per other mode), and a close button. `setMode` is
|
|
40
|
+
* read at mount. A contract `NexusChatWindow` is used as is.
|
|
41
|
+
*/
|
|
42
|
+
window?:
|
|
43
|
+
| {
|
|
44
|
+
docked: boolean;
|
|
45
|
+
canDock: boolean;
|
|
46
|
+
mode?: NexusChatMode;
|
|
47
|
+
modes?: readonly NexusChatMode[];
|
|
48
|
+
/** Minimized but still mounted: a dictation in progress ends. */
|
|
49
|
+
hidden?: boolean;
|
|
50
|
+
setDocked(docked: boolean): void;
|
|
51
|
+
setMode?(mode: NexusChatMode): void;
|
|
52
|
+
close(): void;
|
|
53
|
+
}
|
|
54
|
+
| NexusChatWindow;
|
|
55
|
+
modes?: undefined;
|
|
56
|
+
defaultMode?: undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The chat mounted into this page, sharing its session, in a window drawn
|
|
61
|
+
* here as for `embedUrl`: a launcher icon that opens it floating, docked on
|
|
62
|
+
* the right, or fullscreen. Giving `modes` or `defaultMode` instead of a
|
|
63
|
+
* `window` opts in.
|
|
64
|
+
*/
|
|
65
|
+
export interface WindowedNexusChatProps extends DirectChatProps {
|
|
66
|
+
/** Where the chat may open, in launcher order. Default: all three. */
|
|
67
|
+
modes?: NexusChatMode[];
|
|
68
|
+
/** Open on load in this mode. Default: the launcher icon only. */
|
|
69
|
+
defaultMode?: NexusChatMode;
|
|
70
|
+
window?: undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type NexusChatMode = NexusChatWindowMode;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The chat in an <iframe> served by Incorta, on the user's own Incorta
|
|
77
|
+
* session (the app never holds a token). It draws its window: a launcher
|
|
78
|
+
* icon that opens the chat floating, docked on the right, or fullscreen.
|
|
79
|
+
*/
|
|
80
|
+
export interface EmbeddedNexusChatProps {
|
|
81
|
+
/** Incorta's chat page: `<incorta>/copilot/nexus-chat/embed.html`. */
|
|
82
|
+
embedUrl: string;
|
|
83
|
+
/** Where the chat may open, in launcher order. Default: all three. */
|
|
84
|
+
modes?: NexusChatMode[];
|
|
85
|
+
/** Open on load in this mode. Default: the launcher icon only. */
|
|
86
|
+
defaultMode?: NexusChatMode;
|
|
87
|
+
/** Default `'light'`; followed live. */
|
|
88
|
+
theme?: HostTheme;
|
|
89
|
+
/** Read once, when the chat first opens. Default: the browser's language. */
|
|
90
|
+
locale?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type NexusChatProps = DirectNexusChatProps | WindowedNexusChatProps | EmbeddedNexusChatProps;
|
|
94
|
+
|
|
37
95
|
/**
|
|
38
|
-
* The chat as a React component.
|
|
39
|
-
* `actions`, `events`, `instanceId`, `assetsUrl` and `timeoutMs` are
|
|
40
|
-
* once at mount
|
|
96
|
+
* The chat as a React component. Direct: it fills its box; `connection`,
|
|
97
|
+
* `chat`, `actions`, `events`, `instanceId`, `assetsUrl` and `timeoutMs` are
|
|
98
|
+
* read once at mount, and the other props update the running chat. With
|
|
99
|
+
* `modes` or `defaultMode` (and no `window`), or embedded (`embedUrl`): it
|
|
100
|
+
* draws its own window.
|
|
41
101
|
*/
|
|
42
102
|
export function NexusChat(props: NexusChatProps): ReactElement;
|