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

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 (3) hide show
  1. package/README.md +5 -4
  2. package/package.json +1 -1
  3. package/react.js +94 -76
package/README.md CHANGED
@@ -24,9 +24,10 @@ import { NexusChat } from '@incorta/nexus-chat/react';
24
24
  ```
25
25
 
26
26
  It draws its own window: an Incorta Intelligence launcher at the bottom right opens the chat
27
- floating, docked on the right (the page makes room) or fullscreen, switched or minimized back to
28
- the launcher from the top right. `modes` limits the windows (default all three, in that order);
29
- `defaultMode` opens one on load. Below 768px wide the chat covers the page. The window takes the
27
+ floating or docked on the right (the page makes room). The chat's own top bar takes it to full
28
+ screen and back (Undock / Dock to the side) and closes it back to the launcher; nothing sits
29
+ above the chat. `modes` limits the windows (default all three, in that order) and `defaultMode`
30
+ opens one on load; dock returns to the window last shown beside the page. Below 768px wide the chat covers the page. The window takes the
30
31
  page's background colour, the chat is drawn smaller when floating or docked, and the launcher icon
31
32
  shimmers while the chat loads. `theme` is followed live; `locale` is read when the chat first opens.
32
33
 
@@ -47,7 +48,7 @@ need that setting.
47
48
  />
48
49
  ```
49
50
 
50
- The chat fills the component's box; the app draws any frame around it. `theme`, `locale` (default:
51
+ The chat fills the component's box; the app draws any frame around it. Until the chat is mounted, the Incorta Intelligence mark sits in the middle of the box (the embedded window's loading face), and it leaves when the chat mounts or `onLoadError` fires, so the app's own failure takes the box. `theme`, `locale` (default:
51
52
  the browser's), `direction`, `tenantId`, `user` and `window` update the running chat;
52
53
  `connection`, `chat`, `actions`, `events`, `instanceId`, `assetsUrl` and `timeoutMs` are read once
53
54
  at mount. `window` (`docked`, `canDock`, `setDocked`, `close`) hands the chat's window to the app:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahmedramez-org/nexus-chat",
3
- "version": "1.0.0-beta.3",
3
+ "version": "1.0.0-beta.4",
4
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": {
package/react.js CHANGED
@@ -43,6 +43,9 @@ function DirectChat({
43
43
  const windowState = useRef(null);
44
44
  windowState.current ??= emitter();
45
45
  const hasWindow = Boolean(hostWindow);
46
+ // Until the chat is mounted its brand mark holds the middle of the box, the
47
+ // same loading face as the embedded window, so every app shows one.
48
+ const [loading, setLoading] = useState(true);
46
49
 
47
50
  useEffect(
48
51
  () => environment.current.notify(),
@@ -108,7 +111,10 @@ function DirectChat({
108
111
  : {}),
109
112
  });
110
113
  const fail = (error) => {
111
- if (!cancelled) current().onLoadError?.(error);
114
+ if (cancelled) return;
115
+ // The app draws its own failure (and Retry) where the chat would be.
116
+ setLoading(false);
117
+ current().onLoadError?.(error);
112
118
  };
113
119
  // In a promise, so a missing source reaches onLoadError like any load failure.
114
120
  Promise.resolve()
@@ -121,6 +127,7 @@ function DirectChat({
121
127
  forgetNexusChat(source);
122
128
  return fail(error);
123
129
  }
130
+ setLoading(false);
124
131
  current().onReady?.(handle);
125
132
  }, fail);
126
133
  return () => {
@@ -138,11 +145,28 @@ function DirectChat({
138
145
  };
139
146
  }, [hasWindow]);
140
147
 
141
- return h('div', {
142
- ref: element,
143
- className,
144
- style: { width: '100%', height: '100%', ...style },
145
- });
148
+ // The app's className and style stay on the chat's own root (the mount element).
149
+ return h(
150
+ 'div',
151
+ { style: { position: 'relative', width: '100%', height: '100%' } },
152
+ h('div', { ref: element, className, style: { width: '100%', height: '100%', ...style } }),
153
+ loading &&
154
+ h(
155
+ 'div',
156
+ {
157
+ role: 'status',
158
+ 'aria-label': 'Loading the AI Assistant',
159
+ style: {
160
+ position: 'absolute',
161
+ inset: 0,
162
+ display: 'grid',
163
+ placeItems: 'center',
164
+ pointerEvents: 'none',
165
+ },
166
+ },
167
+ h('img', { src: ICON, alt: '', width: 48, height: 54 }),
168
+ ),
169
+ );
146
170
  }
147
171
 
148
172
  // ---- embedded: the chat in an Incorta-served iframe, in a window drawn here
@@ -162,19 +186,6 @@ const ICON = `data:image/svg+xml,${encodeURIComponent(
162
186
  "<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.0333 35.3749C89.81 37.6474 95.214 41.1522 99.8334 45.8891C102.44 48.5657 104.644 51.4847 106.465 54.5652L106.677 35.385C106.717 31.9611 104.936 28.7896 101.997 27.0423L63.1104 3.93311C66.1798 5.83193 69.078 8.13474 71.7147 10.8517C78.508 17.8208 82.604 26.4261 84.0333 35.3749Z' fill='url(#paint0_linear_108_873)'/><path d='M22.8859 84.8047C17.1092 82.532 11.7049 79.0273 7.0856 74.2907C4.35831 71.4927 2.07382 68.432 0.212009 65.2002L0.000668115 84.6433C-0.0395873 88.0667 1.74171 91.2387 4.68035 92.986L43.9092 116.297C40.7995 114.378 37.8709 112.055 35.204 109.328C28.4109 102.359 24.3149 93.7533 22.8859 84.8047Z' fill='url(#paint1_linear_108_873)'/><path d='M106.114 85.8249L106.456 54.5647C104.645 51.4841 102.431 48.5651 99.824 45.8886C95.2047 41.1517 89.8006 37.6469 84.024 35.3743C86.258 49.4237 81.9006 64.3012 71.0013 75.0076C57.878 87.8849 38.9278 91.1269 22.876 84.8149C24.305 93.7636 28.401 102.369 35.1941 109.338C37.861 112.075 40.7896 114.398 43.8993 116.307L45.1674 117.054L47.9148 118.69C48.891 119.266 49.9376 119.65 51.0144 119.862C53.1882 120.266 55.4626 119.923 57.4553 118.802L101.253 94.0663C104.222 92.3896 106.074 89.2489 106.114 85.8349V85.8249Z' fill='url(#paint2_linear_108_873)'/><path d='M35.908 45.1805C49.0312 32.3027 67.9813 29.0606 84.0333 35.3732C82.604 26.4245 78.508 17.8191 71.7153 10.85C69.0786 8.14313 66.18 5.84031 63.1106 3.93137L61.6714 3.07286L58.7529 1.33563C57.7063 0.709427 56.5691 0.315517 55.4118 0.123615C53.3184 -0.219791 51.1346 0.153915 49.2225 1.22453L5.41455 25.9699C2.44572 27.6465 0.593974 30.7877 0.553719 34.2015L0.211548 65.1889C2.07336 68.4313 4.35785 71.4813 7.08516 74.2787C11.7044 79.016 17.1088 82.5207 22.8854 84.7933C20.6512 70.744 25.0088 55.8664 35.908 45.1603V45.1805Z' fill='#00A098'/><path d='M22.8857 84.8051C22.9663 85.3104 23.0568 85.8251 23.1575 86.3304C23.1877 86.4917 23.2279 86.6637 23.2581 86.8251C23.3285 87.1684 23.399 87.5117 23.4795 87.8551C23.5197 88.0471 23.5701 88.2391 23.6103 88.4311C23.6909 88.7544 23.7713 89.0777 23.8619 89.4004C23.9123 89.5924 23.9626 89.7744 24.0129 89.9664C24.1236 90.3604 24.2443 90.7544 24.3651 91.1377C24.4457 91.3904 24.5261 91.6431 24.6067 91.8851C24.7073 92.1984 24.818 92.5117 24.9287 92.8244C25.0193 93.0671 25.1099 93.3197 25.2004 93.5617C25.291 93.7944 25.3715 94.0164 25.4621 94.2491C25.5627 94.5111 25.6734 94.7837 25.7841 95.0464C25.8647 95.2384 25.9451 95.4304 26.0357 95.6224C26.1565 95.9151 26.2873 96.1977 26.4181 96.4811C26.4584 96.5717 26.4987 96.6524 26.5389 96.7331C26.8609 97.4304 27.2031 98.1171 27.5654 98.8037C27.6057 98.8744 27.6359 98.9451 27.6761 99.0157C27.8573 99.3491 28.0384 99.6824 28.2297 100.006C28.29 100.117 28.3504 100.218 28.4108 100.329C28.602 100.662 28.7932 100.996 28.9945 101.319C29.0448 101.4 29.0951 101.47 29.1354 101.551C29.8399 102.682 30.5947 103.804 31.4098 104.884C31.8225 104.874 92.7287 102.874 96.2107 42.546C96.5533 36.6576 96.352 30.2238 95.486 23.1739L63.1109 3.93311C66.1804 5.83193 69.0787 8.13474 71.7153 10.8517C78.5087 17.8208 82.6047 26.4261 84.0333 35.3749C86.268 49.4242 81.91 64.3017 71.0113 75.0077C57.8878 87.8857 38.9376 91.1277 22.8857 84.8151V84.8051Z' fill='url(#paint3_linear_108_873)'/><path d='M84.0334 35.3745C83.9527 34.8695 83.862 34.3544 83.7614 33.8494C83.7314 33.6878 83.6914 33.5161 83.6607 33.3545C83.5907 33.0111 83.52 32.6677 83.4394 32.3243C83.3994 32.1324 83.3487 31.9405 83.3087 31.7486C83.228 31.4254 83.1474 31.1021 83.0574 30.7789C83.0067 30.5871 82.9567 30.4053 82.906 30.2133C82.7954 29.8195 82.6747 29.4255 82.554 29.0417C82.4734 28.7892 82.3927 28.5367 82.3127 28.2943C82.212 27.9812 82.1014 27.6681 81.99 27.355C81.9 27.1126 81.8094 26.8601 81.7187 26.6177C81.628 26.3854 81.5474 26.1632 81.4567 25.9309C81.356 25.6683 81.2454 25.3956 81.1347 25.133C81.0547 24.9411 80.974 24.7491 80.8834 24.5573C80.7627 24.2643 80.632 23.9815 80.5007 23.6987C80.4607 23.6079 80.4207 23.5271 80.38 23.4463C80.058 22.7493 79.716 22.0625 79.3534 21.3757C79.3134 21.305 79.2834 21.2343 79.2427 21.1636C79.062 20.8303 78.8807 20.497 78.6894 20.1738C78.6294 20.0627 78.5687 19.9617 78.508 19.8506C78.3174 19.5173 78.126 19.184 77.9247 18.8607C77.874 18.7799 77.824 18.7093 77.7834 18.6285C77.0794 17.4973 76.3247 16.3761 75.5094 15.2954C75.0967 15.3055 14.1902 17.3053 10.7082 77.6333C10.366 83.522 10.5673 89.956 11.4328 97.006L43.8082 116.247C40.7386 114.348 37.8403 112.045 35.2036 109.328C28.4104 102.359 24.3145 93.7533 22.8854 84.8047C20.6512 70.7553 25.0089 55.8779 35.908 45.1717C49.0313 32.294 67.9814 29.0518 84.0334 35.3644V35.3745Z' fill='url(#paint4_linear_108_873)'/></g><defs><linearGradient id='paint0_linear_108_873' x1='104.644' y1='41.0916' x2='52.1952' y2='1.61001' gradientUnits='userSpaceOnUse'><stop stop-color='#4854FE'/><stop offset='1' stop-color='#241C55'/></linearGradient><linearGradient id='paint1_linear_108_873' x1='0.000668115' y1='90.7533' x2='43.9092' y2='90.7533' gradientUnits='userSpaceOnUse'><stop stop-color='#4854FE'/><stop offset='1' stop-color='#241C55'/></linearGradient><linearGradient id='paint2_linear_108_873' x1='31.5078' y1='40.5846' x2='92.6593' y2='102.893' gradientUnits='userSpaceOnUse'><stop stop-color='white'/><stop offset='1' stop-color='#4854FE'/></linearGradient><linearGradient id='paint3_linear_108_873' x1='86.272' y1='19.1173' x2='41.5609' y2='98.7424' 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.9156' y1='103.321' x2='61.0359' y2='27.5007' gradientUnits='userSpaceOnUse'><stop stop-color='#006EF0'/><stop offset='1' stop-color='#006EF0' stop-opacity='0'/></linearGradient><clipPath id='clip0_108_873'><rect width='106.667' height='120' fill='white'/></clipPath></defs></svg>",
163
187
  )}`;
164
188
 
165
- const TOOL_ICONS = {
166
- floating: 'M4 4h16v16H4zM12 12h6v6h-6z',
167
- docked: 'M4 4h16v16H4zM14 4v16',
168
- fullscreen: 'M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5',
169
- minimize: 'M5 12h14',
170
- };
171
- const TOOL_LABELS = {
172
- floating: 'Float',
173
- docked: 'Dock',
174
- fullscreen: 'Full screen',
175
- minimize: 'Minimize',
176
- };
177
-
178
189
  function isNarrow() {
179
190
  return typeof window !== 'undefined' && window.innerWidth < NARROW_VIEWPORT;
180
191
  }
@@ -188,57 +199,26 @@ function pageBackground() {
188
199
  return 'Canvas';
189
200
  }
190
201
 
191
- function ToolButton({ tool, onClick }) {
192
- return h(
193
- 'button',
194
- {
195
- type: 'button',
196
- title: TOOL_LABELS[tool],
197
- 'aria-label': TOOL_LABELS[tool],
198
- onClick,
199
- style: {
200
- display: 'grid',
201
- placeItems: 'center',
202
- width: 28,
203
- height: 28,
204
- padding: 0,
205
- border: 0,
206
- borderRadius: 6,
207
- background: 'transparent',
208
- color: 'inherit',
209
- cursor: 'pointer',
210
- },
211
- },
212
- h(
213
- 'svg',
214
- {
215
- width: 16,
216
- height: 16,
217
- viewBox: '0 0 24 24',
218
- fill: 'none',
219
- stroke: 'currentColor',
220
- strokeWidth: 2,
221
- strokeLinecap: 'round',
222
- strokeLinejoin: 'round',
223
- 'aria-hidden': true,
224
- },
225
- h('path', { d: TOOL_ICONS[tool] }),
226
- ),
227
- );
228
- }
229
-
230
202
  function EmbeddedChat({ embedUrl, modes = MODES, defaultMode, theme = 'light', locale }) {
231
203
  const allowed = modes.filter((mode) => MODES.includes(mode));
204
+ const canFullscreen = allowed.includes('fullscreen');
232
205
  const [view, setView] = useState(allowed.includes(defaultMode) ? defaultMode : 'minimized');
233
206
  // Created on first open and kept while minimized, so the conversation stays.
234
207
  const [opened, setOpened] = useState(view !== 'minimized');
235
208
  const [ready, setReady] = useState(false);
209
+ // The chat itself is up, and draws the window's controls; until then (loading,
210
+ // signed out, failed) the window keeps a close of its own.
211
+ const [mounted, setMounted] = useState(false);
236
212
  const [narrow, setNarrow] = useState(isNarrow);
237
213
  const [background, setBackground] = useState('Canvas');
238
214
  const frame = useRef(null);
239
215
  const icon = useRef(null);
240
216
  const fallback = useRef(undefined);
241
217
  const shown = view === 'minimized' ? undefined : narrow ? 'fullscreen' : view;
218
+ // Where "dock" goes back to from full screen: the window last shown beside the page.
219
+ const [side, setSide] = useState(
220
+ view === 'floating' || view === 'docked' ? view : allowed.find((mode) => mode !== 'fullscreen'),
221
+ );
242
222
  // Set once: a new src would reload the chat. Later theme changes go by message.
243
223
  // The app's URL is kept as given (relative ones resolve like any iframe src).
244
224
  const [src] = useState(() => {
@@ -250,6 +230,7 @@ function EmbeddedChat({ embedUrl, modes = MODES, defaultMode, theme = 'light', l
250
230
 
251
231
  const show = (next) => {
252
232
  setView(next);
233
+ if (next === 'floating' || next === 'docked') setSide(next);
253
234
  if (next !== 'minimized') setOpened(true);
254
235
  };
255
236
 
@@ -281,6 +262,7 @@ function EmbeddedChat({ embedUrl, modes = MODES, defaultMode, theme = 'light', l
281
262
  const onMessage = (event) => {
282
263
  if (event.source === frame.current?.contentWindow && event.data?.nexusChat === 'ready') {
283
264
  setReady(true);
265
+ if (event.data.mounted === true) setMounted(true);
284
266
  }
285
267
  };
286
268
  window.addEventListener('message', onMessage);
@@ -290,13 +272,39 @@ function EmbeddedChat({ embedUrl, modes = MODES, defaultMode, theme = 'light', l
290
272
  };
291
273
  }, []);
292
274
 
293
- // Theme and type size follow the app without reloading the iframe.
275
+ // The chat's own window controls, relayed by the page inside: dock (back
276
+ // beside the page, or full screen) and close (minimize).
277
+ useEffect(() => {
278
+ // From the chat page itself, not a sign-in page the frame navigated to.
279
+ const origin = new URL(embedUrl, location.href).origin;
280
+ const onMessage = (event) => {
281
+ if (event.source !== frame.current?.contentWindow || event.origin !== origin) return;
282
+ if (event.data?.nexusChat === 'dock' && (event.data.docked || canFullscreen))
283
+ show(event.data.docked ? side : 'fullscreen');
284
+ if (event.data?.nexusChat === 'close') show('minimized');
285
+ };
286
+ window.addEventListener('message', onMessage);
287
+ return () => window.removeEventListener('message', onMessage);
288
+ }, [side, canFullscreen, embedUrl]);
289
+
290
+ // Theme, type size and the window's state follow the app without reloading
291
+ // the iframe. The chat draws its own dock and close controls from the state:
292
+ // "docked" means beside the page (floating or docked), the opposite of full screen.
294
293
  const sendStyle = () =>
295
294
  frame.current?.contentWindow?.postMessage(
296
- { nexusChat: 'style', theme, remBase: REM_BASE[shown ?? 'floating'] },
295
+ {
296
+ nexusChat: 'style',
297
+ theme,
298
+ remBase: REM_BASE[shown ?? 'floating'],
299
+ // Undock goes to full screen, so it is offered only where the app allows full screen.
300
+ window: {
301
+ docked: shown !== 'fullscreen',
302
+ canDock: !narrow && side !== undefined && canFullscreen,
303
+ },
304
+ },
297
305
  new URL(embedUrl, location.href).origin,
298
306
  );
299
- useEffect(sendStyle, [theme, shown]);
307
+ useEffect(sendStyle, [theme, shown, narrow, side, canFullscreen]);
300
308
 
301
309
  useEffect(() => {
302
310
  if (ready || !opened || !icon.current?.animate) return;
@@ -323,8 +331,6 @@ function EmbeddedChat({ embedUrl, modes = MODES, defaultMode, theme = 'light', l
323
331
  docked: { top: 0, right: 0, bottom: 0, width: PANEL_WIDTH },
324
332
  fullscreen: { inset: 0 },
325
333
  };
326
- // Narrow screens cover the app anyway, so only minimizing is offered there.
327
- const tools = [...(narrow ? [] : allowed.filter((mode) => mode !== shown)), 'minimize'];
328
334
 
329
335
  return h(
330
336
  'div',
@@ -341,24 +347,11 @@ function EmbeddedChat({ embedUrl, modes = MODES, defaultMode, theme = 'light', l
341
347
  boxSizing: 'border-box',
342
348
  display: shown ? 'flex' : 'none',
343
349
  flexDirection: 'column',
344
- gap: 4,
345
350
  padding: GUTTER,
346
- paddingTop: 4,
347
351
  background,
348
352
  ...placement[shown ?? 'floating'],
349
353
  },
350
354
  },
351
- h(
352
- 'div',
353
- { style: { display: 'flex', justifyContent: 'flex-end', gap: 2 } },
354
- tools.map((tool) =>
355
- h(ToolButton, {
356
- key: tool,
357
- tool,
358
- onClick: () => show(tool === 'minimize' ? 'minimized' : tool),
359
- }),
360
- ),
361
- ),
362
355
  h(
363
356
  'div',
364
357
  {
@@ -382,6 +375,31 @@ function EmbeddedChat({ embedUrl, modes = MODES, defaultMode, theme = 'light', l
382
375
  },
383
376
  style: { display: 'block', width: '100%', height: '100%', border: 0 },
384
377
  }),
378
+ !mounted &&
379
+ h(
380
+ 'button',
381
+ {
382
+ type: 'button',
383
+ 'aria-label': 'Close AI Assistant',
384
+ title: 'Close',
385
+ onClick: () => show('minimized'),
386
+ style: {
387
+ position: 'absolute',
388
+ top: 8,
389
+ right: 8,
390
+ zIndex: 1,
391
+ width: 32,
392
+ height: 32,
393
+ border: 0,
394
+ borderRadius: '50%',
395
+ background: 'transparent',
396
+ color: 'inherit',
397
+ font: '20px/1 system-ui, sans-serif',
398
+ cursor: 'pointer',
399
+ },
400
+ },
401
+ '×',
402
+ ),
385
403
  !ready &&
386
404
  h(
387
405
  'div',