@ahmedramez-org/nexus-chat 0.0.0-stage → 1.0.0-beta.2

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 (80) hide show
  1. package/README.md +101 -2
  2. package/contract/conformance.d.ts +51 -0
  3. package/contract/conformance.js +304 -0
  4. package/contract/host.d.ts +54 -0
  5. package/contract/host.js +78 -0
  6. package/contract/index.d.ts +394 -0
  7. package/contract/index.js +35 -0
  8. package/index.d.ts +31 -0
  9. package/index.js +87 -0
  10. package/package.json +55 -4
  11. package/react.d.ts +42 -0
  12. package/react.js +114 -0
  13. package/remote/assets/AgentOptionControl-BnqfB5W3.js +1 -0
  14. package/remote/assets/CsvPreview-DD2VELWa.js +10 -0
  15. package/remote/assets/ErrorCard-Bu_0QAJ9.js +1 -0
  16. package/remote/assets/HighlightedCode-DpxGYHfF.js +1 -0
  17. package/remote/assets/HistorySearchDialog-q4cvx5jy.js +41 -0
  18. package/remote/assets/HostBridgeProvider-BV-UGU3Q.js +1 -0
  19. package/remote/assets/HtmlArtifactRenderer-EO_cwaEm.js +1 -0
  20. package/remote/assets/I18nContext-BlQKoDmv.js +24 -0
  21. package/remote/assets/PreviewState-VcIZyCXJ.js +1 -0
  22. package/remote/assets/SourceCitations-DvApeC9N.js +1 -0
  23. package/remote/assets/SourcesArtifactRenderer-BqnIOctm.js +1 -0
  24. package/remote/assets/TableArtifactRenderer-BLZLQMxU.js +2 -0
  25. package/remote/assets/TiptapComposerInput-CmxEiBlE.js +132 -0
  26. package/remote/assets/UnsupportedArtifact-CeOCu_J6.js +1 -0
  27. package/remote/assets/_virtual_mf-localSharedImportMap___mfe_internal__nexusChat__mf_owner__1-CObPUVU6.js +1 -0
  28. package/remote/assets/ar-BV81JAir.js +1 -0
  29. package/remote/assets/button-CO0tH6F2.js +1 -0
  30. package/remote/assets/client-Bv7EbCrM.js +2 -0
  31. package/remote/assets/composite-giRhJC3d.js +1 -0
  32. package/remote/assets/conformance-entry-uotihsU2.js +2 -0
  33. package/remote/assets/createLucideIcon-D1A0JYKN.js +1 -0
  34. package/remote/assets/de-BXJlhmC2.js +1 -0
  35. package/remote/assets/defineProperty-BbfpZ9Tg.js +1 -0
  36. package/remote/assets/dist-MaNcI-Om.js +4 -0
  37. package/remote/assets/errorReporter-hCgThzof.js +1 -0
  38. package/remote/assets/es-nnw68YFM.js +1 -0
  39. package/remote/assets/federation-entry-B8pojL73.css +2 -0
  40. package/remote/assets/federation-entry-BA9UpYiM.js +65 -0
  41. package/remote/assets/floating-ui.dom-B99QSfPH.js +1 -0
  42. package/remote/assets/floating-ui.utils.dom-ObnfY4F6.js +1 -0
  43. package/remote/assets/fr-CJIAbbzn.js +1 -0
  44. package/remote/assets/highlighter-C37NKSjx.js +19 -0
  45. package/remote/assets/hostInit-YMtJ2r-g.js +1 -0
  46. package/remote/assets/it-D3TiEKfy.js +1 -0
  47. package/remote/assets/ja-ZB0L4Lsc.js +1 -0
  48. package/remote/assets/jsx-runtime-DE3RlOCf.js +1 -0
  49. package/remote/assets/lib-CjT9YRPC.js +1 -0
  50. package/remote/assets/modelProviderIcons-fiyx4SaW.js +1 -0
  51. package/remote/assets/react-dom-FxOPXJ08.js +1 -0
  52. package/remote/assets/rehype-raw-CNOzW4vu.js +2 -0
  53. package/remote/assets/roboto-latin-400-italic-CSuqwwKr.woff2 +0 -0
  54. package/remote/assets/roboto-latin-400-normal-BqEyEoaF.woff2 +0 -0
  55. package/remote/assets/roboto-latin-500-italic-DOe3GFcv.woff2 +0 -0
  56. package/remote/assets/roboto-latin-500-normal-7RbcRiD8.woff2 +0 -0
  57. package/remote/assets/roboto-latin-700-italic-Cb9agdGy.woff2 +0 -0
  58. package/remote/assets/roboto-latin-700-normal-BZpUvMxY.woff2 +0 -0
  59. package/remote/assets/roboto-latin-ext-400-italic-BilqUbDw.woff2 +0 -0
  60. package/remote/assets/roboto-latin-ext-400-normal-C3tdtHj3.woff2 +0 -0
  61. package/remote/assets/roboto-latin-ext-500-italic-B5teBpxj.woff2 +0 -0
  62. package/remote/assets/roboto-latin-ext-500-normal-pMCM9Ixg.woff2 +0 -0
  63. package/remote/assets/roboto-latin-ext-700-italic-CBdFfgzf.woff2 +0 -0
  64. package/remote/assets/roboto-latin-ext-700-normal-DSBUz0N1.woff2 +0 -0
  65. package/remote/assets/rolldown-runtime-hePW80VL.js +1 -0
  66. package/remote/assets/sessionTitle-C9LhAN0r.js +1 -0
  67. package/remote/assets/spinner-eqRx6Fq-.js +1 -0
  68. package/remote/assets/sqlFormatting-BjJIfUN_.js +7 -0
  69. package/remote/assets/src-CIHHGjAO.js +1 -0
  70. package/remote/assets/store-FaSYSP4_.js +1 -0
  71. package/remote/assets/useBaseUiId-Dpqq0brK.js +1 -0
  72. package/remote/assets/usePositioner-BhCjFmeW.js +1 -0
  73. package/remote/assets/virtualExposes-C0oWo8z2.js +1 -0
  74. package/remote/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__nexusChat__remoteEntry_js-5uUvXWWH.js +1 -0
  75. package/remote/assets/virtual_mf-exposes-ssr___mfe_internal__nexusChat__remoteEntry_js-WBZ0Muv9.js +2 -0
  76. package/remote/assets/virtual_mf-exposes___mfe_internal__nexusChat__remoteEntry_js-BGZ2nkwm.js +2 -0
  77. package/remote/assets/vite-preload-helper-B7qeedMF.js +1 -0
  78. package/remote/assets/zh-De2Ilp0M.js +1 -0
  79. package/remote/remoteEntry.js +1 -0
  80. package/remote/remoteEntry.ssr.js +1 -0
@@ -0,0 +1,394 @@
1
+ export declare const NEXUS_CHAT_CONTRACT_VERSION: 2;
2
+ export type NexusChatContractVersion = typeof NEXUS_CHAT_CONTRACT_VERSION;
3
+ export type HostTheme = 'light' | 'dark';
4
+ export type HostDirection = 'ltr' | 'rtl';
5
+ export type ArtifactPlacement = 'inline' | 'canvas';
6
+ export type ArtifactView = 'chart' | 'table';
7
+ export interface NexusChatConnection {
8
+ /**
9
+ * The IncortaCopilot client this host is (`general` for Incorta, `hikari`
10
+ * for Hikari). Copilot picks the welcome text, agents and contexts from it.
11
+ */
12
+ client: string;
13
+ /** IncortaCopilot's root, for example `/incorta/copilot` (Incorta) or `/copilot` (Hikari). */
14
+ copilotUrl: string;
15
+ /** Agent used when the Copilot handshake names no default agent for this client. */
16
+ agent?: string;
17
+ /** Headers every request carries (auth, CSRF). Re-read before each attempt. */
18
+ headers?(): Readonly<Record<string, string>> | Promise<Readonly<Record<string, string>>>;
19
+ credentials?: RequestCredentials;
20
+ /**
21
+ * Called once after a 401/403; resolve `true` when a retry can succeed.
22
+ * Absent: the failure surfaces to the user.
23
+ */
24
+ refreshAuth?(): Promise<boolean>;
25
+ /** Poll tuning; hosts normally omit both (defaults 1500 ms / 1000 ms). */
26
+ pollIntervalMs?: number;
27
+ initialPollDelayMs?: number;
28
+ }
29
+ export interface HostUser {
30
+ id?: string | number;
31
+ name?: string;
32
+ }
33
+ /**
34
+ * Plain host state. `get` may return a fresh object on every call: the chat
35
+ * compares the fields itself.
36
+ */
37
+ export interface NexusChatEnvironmentSnapshot {
38
+ theme: HostTheme;
39
+ locale: string;
40
+ /** Absent: follows `locale` (Arabic, Hebrew, Persian and Urdu are right-to-left). */
41
+ direction?: HostDirection;
42
+ tenantId?: string;
43
+ user?: HostUser;
44
+ }
45
+ export interface NexusChatEnvironment {
46
+ get(): NexusChatEnvironmentSnapshot;
47
+ /** Call `onChange` whenever `get` may return something different. */
48
+ subscribe(onChange: () => void): () => void;
49
+ }
50
+ export type ContextKind = 'business-view' | 'physical-table' | 'insight' | 'file' | 'knowledge-store' | 'ai-container';
51
+ /** One row in a context browser. */
52
+ export interface ContextItem {
53
+ id: string;
54
+ label: string;
55
+ description?: string;
56
+ /** A folder the user can open; its children load with `parentId: id`. */
57
+ hasChildren?: boolean;
58
+ /** Shown greyed out with this reason. */
59
+ disabledReason?: string;
60
+ /**
61
+ * What Copilot receives when the item is attached, sent as
62
+ * `{ type, data: dataset }` (`type` follows the kind: `business_view`,
63
+ * `insight`, `space`, `ai_container`; a physical table travels as a
64
+ * `business_view`, as Copilot reads it). An item without one cannot be picked.
65
+ */
66
+ dataset?: Readonly<Record<string, unknown>>;
67
+ }
68
+ /** A host-owned context browser: the host loads the rows, the chat does the rest. */
69
+ export interface ContextSource {
70
+ kind: Exclude<ContextKind, 'file'>;
71
+ load(request: {
72
+ parentId?: string;
73
+ query: string;
74
+ }, signal: AbortSignal): Promise<readonly ContextItem[]>;
75
+ /**
76
+ * Re-reads an attached item's `dataset` before every send (a retry asks
77
+ * again), so the question carries what the host shows now: a dashboard
78
+ * insight re-applies the current runtime filters. A rejection fails the
79
+ * send. Absent: the dataset goes as picked.
80
+ */
81
+ resolve?(dataset: Readonly<Record<string, unknown>>, signal: AbortSignal): Promise<Readonly<Record<string, unknown>>>;
82
+ }
83
+ /** `'file'` is the chat's own upload browser; every other kind is a host source. */
84
+ export type ContextSpec = 'file' | ContextSource;
85
+ export interface ColumnSummary {
86
+ id: string;
87
+ name: string;
88
+ label?: string;
89
+ type?: string;
90
+ }
91
+ export interface ValueMatch {
92
+ value: string | number | boolean | null;
93
+ label: string;
94
+ fieldId?: string;
95
+ fieldLabel?: string;
96
+ dataType?: string;
97
+ complete?: boolean;
98
+ }
99
+ /**
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.
103
+ *
104
+ * - `@` mentions a column. Asked once per view; the chat filters as you type.
105
+ * - `#` searches the view's values for the typed text.
106
+ */
107
+ export type TriggerSpec = {
108
+ char: '@';
109
+ search(request: {
110
+ viewId: string;
111
+ }, signal: AbortSignal): Promise<readonly ColumnSummary[]>;
112
+ } | {
113
+ char: '#';
114
+ search(request: {
115
+ viewId: string;
116
+ query: string;
117
+ limit: number;
118
+ }, signal: AbortSignal): Promise<readonly ValueMatch[]>;
119
+ };
120
+ /** Switches for things the chat builds itself. Each one defaults to `true`. */
121
+ export interface NexusChatFeatures {
122
+ /**
123
+ * Follow running answers over one SSE stream instead of polling. A backend
124
+ * that refuses the stream falls back to polling.
125
+ */
126
+ streaming?: boolean;
127
+ /**
128
+ * `false` when the host page's CSP blocks the `blob:` frame a PDF preview
129
+ * needs (Hikari serves `frame-src 'none'`); PDF cards then offer Download.
130
+ */
131
+ inlinePdfPreview?: boolean;
132
+ }
133
+ export interface NexusArtifact {
134
+ id: string;
135
+ /**
136
+ * Artifact type. New kinds SHOULD use the namespaced `vendor.name` form
137
+ * (e.g. `incorta.insight`, `hikari.dashboard`) so two hosts can never
138
+ * collide on a bare name; `parseArtifactKind` reads that convention.
139
+ * Existing bare kinds (`insight`, …) remain valid.
140
+ */
141
+ kind: string;
142
+ /**
143
+ * Version of the payload shape under `data`, independent of the contract
144
+ * version. A renderer that does not recognise a kind's schemaVersion must
145
+ * decline it synchronously (`canRender` → false) so the portable fallback
146
+ * renders — never guess at a payload. Absent means "unversioned legacy
147
+ * payload", which hosts may continue to accept.
148
+ */
149
+ schemaVersion?: number;
150
+ data: unknown;
151
+ metadata?: Readonly<Record<string, unknown>>;
152
+ }
153
+ /**
154
+ * Reads the namespaced-kind convention. Returns null for bare legacy kinds.
155
+ * Framework-free on purpose: hosts use this inside `canRender` to route by
156
+ * vendor without re-implementing the convention.
157
+ */
158
+ export declare function parseArtifactKind(kind: string): {
159
+ namespace: string;
160
+ name: string;
161
+ } | null;
162
+ /**
163
+ * True when the artifact carries the given payload schema version (or is an
164
+ * unversioned legacy payload and `acceptUnversioned` allows it). The guard a
165
+ * host renderer should call first in `canRender`.
166
+ */
167
+ export declare function artifactMatchesSchema(artifact: Pick<NexusArtifact, 'schemaVersion'>, supportedVersion: number, options?: {
168
+ acceptUnversioned?: boolean;
169
+ }): boolean;
170
+ /** The appearance the remote is rendering under, so a host renderer can match it without reading the DOM. */
171
+ export interface HostArtifactRendererAppearance {
172
+ theme: HostTheme;
173
+ direction: HostDirection;
174
+ locale: string;
175
+ }
176
+ export interface HostArtifactRenderRequest {
177
+ artifact: NexusArtifact;
178
+ messageId: string;
179
+ sessionId?: string;
180
+ userQuestion?: string;
181
+ taskId?: string;
182
+ placement: ArtifactPlacement;
183
+ /** Optional canvas presentation. Unsupported views fall back to the host's default. */
184
+ view?: ArtifactView;
185
+ /**
186
+ * Optional, per-mount callback: report the views this artifact actually has,
187
+ * including after asynchronous preparation. Never report after signal abort.
188
+ * Omit calls to retain the default canvas without a view-switching menu.
189
+ */
190
+ onSupportedViewsChange?(views: readonly ArtifactView[]): void;
191
+ isHistorical: boolean;
192
+ cachedData?: unknown;
193
+ metadata?: Readonly<Record<string, unknown>>;
194
+ /** Re-sent on every `update`: a theme switch arrives as an update with a new appearance. */
195
+ appearance?: HostArtifactRendererAppearance;
196
+ /** Aborted when the remote unmounts this renderer; stable across updates. */
197
+ signal?: AbortSignal;
198
+ /**
199
+ * Live answers only (absent when `isHistorical`): fields to merge into the
200
+ * artifact's `data` on the backend once its task finishes, so a reopened
201
+ * conversation replays the artifact without re-running it (an Incorta
202
+ * insight reports its `cached_data`). The first report per artifact wins.
203
+ */
204
+ onDataReady?(patch: Readonly<Record<string, unknown>>): void;
205
+ }
206
+ export interface HostArtifactRendererMount {
207
+ update(request: HostArtifactRenderRequest): void;
208
+ unmount(): void;
209
+ }
210
+ /** Host-native charts, painted into an element the chat owns. */
211
+ export interface HostArtifactRendererCapability {
212
+ /** Optional synchronous, side-effect-free title from a host-owned payload. No I/O or rendering. */
213
+ getTitle?(artifact: NexusArtifact): string | undefined;
214
+ canRender(request: HostArtifactRenderRequest): boolean;
215
+ mount(element: HTMLElement, request: HostArtifactRenderRequest): HostArtifactRendererMount;
216
+ }
217
+ /** What a host adds to one question on its way to Copilot. */
218
+ export interface SendAddition {
219
+ /** Replaces the text sent to Copilot; the user still sees what they typed. */
220
+ prompt?: string;
221
+ /** Extra `client_context` fields (a notebook's code `language`); `datasets` and `params` stay the chat's. */
222
+ clientContext?: Readonly<Record<string, unknown>>;
223
+ }
224
+ export interface NexusChatChat {
225
+ /** What the user can attach. Absent or empty: no contexts. */
226
+ contexts?: readonly ContextSpec[];
227
+ /** Absent or empty: `@` and `#` stay plain text. `/` (agents) always works. */
228
+ triggers?: readonly TriggerSpec[];
229
+ /** Absent: tables use the chat's own renderer, other host kinds show as unsupported. */
230
+ renderers?: HostArtifactRendererCapability;
231
+ features?: NexusChatFeatures;
232
+ /**
233
+ * Called before each question is sent (a retry calls it again). A notebook
234
+ * wraps the active paragraph's code into the prompt and adds its language.
235
+ * A rejection fails the send. Absent: questions go as typed.
236
+ */
237
+ beforeSend?(request: {
238
+ prompt: string;
239
+ }): SendAddition | Promise<SendAddition>;
240
+ }
241
+ export type OpenIntent = {
242
+ type: 'open-dashboard';
243
+ dashboardId: string;
244
+ tabId?: string;
245
+ } | {
246
+ type: 'open-insight';
247
+ insightId: string;
248
+ dashboardId?: string;
249
+ tabId?: string;
250
+ } | {
251
+ type: 'open-external';
252
+ url: string;
253
+ } | {
254
+ type: 'open-host-artifact';
255
+ artifact: NexusArtifact;
256
+ target: 'view' | 'edit';
257
+ };
258
+ /** `can` decides, synchronously, whether the control shows; `run` handles the click. */
259
+ export interface HostAction<T, R = void> {
260
+ can(target: T): boolean;
261
+ run(target: T): R | Promise<R>;
262
+ }
263
+ /** An artifact to share, with Copilot's own task and artifact ids (what `POST /copilot/email` takes). */
264
+ export interface ShareTarget {
265
+ artifact: NexusArtifact;
266
+ taskId: string;
267
+ artifactId: string;
268
+ }
269
+ export interface CodeSnippet {
270
+ code: string;
271
+ language?: string;
272
+ }
273
+ export interface NexusChatActions {
274
+ /** Absent: source links and Open/Edit buttons are hidden. */
275
+ open?: HostAction<OpenIntent>;
276
+ /**
277
+ * Save a generated draft in the host's catalog. `run` resolves to the fields
278
+ * of `artifact.data` the save changed; the chat writes them back so the
279
+ * conversation replays the artifact as saved. Absent: no Save button.
280
+ */
281
+ save?: HostAction<NexusArtifact, Record<string, unknown>>;
282
+ /**
283
+ * Share an artifact through the host's own dialog; `run` resolves `true`
284
+ * once shared. Shown in the canvas and the card menu wherever `can` accepts
285
+ * the target. Absent: no Share.
286
+ */
287
+ share?: HostAction<ShareTarget, boolean>;
288
+ /** Insert an answer's code block into the host (a notebook paragraph). Absent: no Insert button. */
289
+ insertCode?: HostAction<CodeSnippet>;
290
+ }
291
+ export interface NexusChatUserActivity {
292
+ type: 'prompt' | 'human-input' | 'cancel' | 'upload' | 'history-mutation';
293
+ occurredAt: number;
294
+ }
295
+ /**
296
+ * Where a contained failure happened. `scope` is a stable slug the remote
297
+ * owns (`task`, `upload`, `poll-budget`, `render-region/thread`,
298
+ * `host-renderer`, …); the ids are present when the failure belongs to one
299
+ * conversation or task.
300
+ */
301
+ export interface NexusChatErrorContext {
302
+ scope: string;
303
+ sessionId?: string;
304
+ taskId?: string;
305
+ remoteTaskId?: string;
306
+ }
307
+ export interface NexusChatEvents {
308
+ /** A contained failure; the user already sees it inline. Absent: logged to the console. */
309
+ onError?(error: NexusChatPublicError, context: NexusChatErrorContext): void;
310
+ /** The mount cannot continue; the host should tear it down and show its fallback. */
311
+ onFatal?(error: NexusChatPublicError): void;
312
+ onActivity?(activity: NexusChatUserActivity): void;
313
+ /**
314
+ * An answer is being produced: keep the user's host session alive until the
315
+ * returned release runs (the answer settled, or the mount ended). A long
316
+ * answer the user only watches must not sign them out. Absent: nothing held.
317
+ */
318
+ keepSessionAlive?(): () => void;
319
+ /**
320
+ * The chat entered or left fullscreen; the host hides or restores its own
321
+ * chrome. Always called with `false` before the mount's cleanup completes.
322
+ */
323
+ onFullscreen?(active: boolean): void;
324
+ }
325
+ /**
326
+ * Everything a host passes to one NexusChat mount. Only plain data and
327
+ * functions cross this boundary — no React node, store, router or provider —
328
+ * which is what lets the chat run its own React. `connection` and
329
+ * `environment` are required; everything else is optional, and the chat hides
330
+ * what it drives when absent.
331
+ */
332
+ export interface NexusChatOptions {
333
+ version: NexusChatContractVersion;
334
+ connection: NexusChatConnection;
335
+ environment: NexusChatEnvironment;
336
+ chat?: NexusChatChat;
337
+ actions?: NexusChatActions;
338
+ events?: NexusChatEvents;
339
+ /**
340
+ * Stable identity for this mount location, keying route-return recovery so
341
+ * two simultaneous mounts never share a checkpoint. Hosts that render one
342
+ * NexusChat at a time omit it (docs/architecture/adr/checkpoint-lifecycle.md).
343
+ */
344
+ instanceId?: string;
345
+ /**
346
+ * The host sizes the mount and owns its window (a page-covering overlay, a
347
+ * docked side panel) and draws its frame. Present: the chat hides its own
348
+ * 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.
351
+ */
352
+ window?: NexusChatWindow;
353
+ }
354
+ export interface NexusChatWindowState {
355
+ /** The mount is the docked side panel (the toggle shows pressed). */
356
+ docked: boolean;
357
+ /** The host page allows docking now; `false` hides the dock toggle. */
358
+ canDock: boolean;
359
+ }
360
+ export interface NexusChatWindow {
361
+ /** May return a fresh object on every call: the chat compares the fields. */
362
+ get(): NexusChatWindowState;
363
+ /** Call `onChange` whenever `get` may return something different. */
364
+ subscribe(onChange: () => void): () => void;
365
+ setDocked(docked: boolean): void;
366
+ close(): void;
367
+ }
368
+ /** What `mount` returns: the host's handle on one running chat. */
369
+ export interface NexusChatHandle {
370
+ /** Tears the chat down and restores the element. Idempotent. */
371
+ unmount(): void;
372
+ /**
373
+ * Sends a question as if the user typed it and pressed Send (same gates: it
374
+ * is ignored while an answer is running). `prompt` replaces the text sent to
375
+ * Copilot, so a host can send a prepared request under a readable question.
376
+ */
377
+ ask(question: string, options?: {
378
+ prompt?: string;
379
+ }): void;
380
+ /**
381
+ * Attaches a host context to the composer as if the user picked it (same
382
+ * gates: the agent must support the kind; `false` when refused).
383
+ */
384
+ attach(kind: Exclude<ContextKind, 'file'>, item: ContextItem): boolean;
385
+ }
386
+ /** The federated entry point `nexusChat/mount`. */
387
+ export type NexusChatMount = (element: HTMLElement, options: NexusChatOptions) => NexusChatHandle;
388
+ export declare class NexusChatPublicError extends Error {
389
+ readonly code: string;
390
+ constructor(code: string, message: string, options?: ErrorOptions);
391
+ }
392
+ export declare function assertCompatibleHost(options: Pick<NexusChatOptions, 'version'>): asserts options is Pick<NexusChatOptions, 'version'> & {
393
+ version: NexusChatContractVersion;
394
+ };
@@ -0,0 +1,35 @@
1
+ export const NEXUS_CHAT_CONTRACT_VERSION = 2;
2
+ /**
3
+ * Reads the namespaced-kind convention. Returns null for bare legacy kinds.
4
+ * Framework-free on purpose: hosts use this inside `canRender` to route by
5
+ * vendor without re-implementing the convention.
6
+ */
7
+ export function parseArtifactKind(kind) {
8
+ const separator = kind.indexOf('.');
9
+ if (separator <= 0 || separator === kind.length - 1)
10
+ return null;
11
+ return { namespace: kind.slice(0, separator), name: kind.slice(separator + 1) };
12
+ }
13
+ /**
14
+ * True when the artifact carries the given payload schema version (or is an
15
+ * unversioned legacy payload and `acceptUnversioned` allows it). The guard a
16
+ * host renderer should call first in `canRender`.
17
+ */
18
+ export function artifactMatchesSchema(artifact, supportedVersion, options) {
19
+ if (artifact.schemaVersion === undefined)
20
+ return options?.acceptUnversioned ?? false;
21
+ return artifact.schemaVersion === supportedVersion;
22
+ }
23
+ export class NexusChatPublicError extends Error {
24
+ code;
25
+ constructor(code, message, options) {
26
+ super(message, options);
27
+ this.name = 'NexusChatPublicError';
28
+ this.code = code;
29
+ }
30
+ }
31
+ export function assertCompatibleHost(options) {
32
+ if (options.version !== NEXUS_CHAT_CONTRACT_VERSION) {
33
+ throw new NexusChatPublicError('INCOMPATIBLE_HOST_CONTRACT', `NexusChat requires host contract ${NEXUS_CHAT_CONTRACT_VERSION}; received ${String(options.version)}.`);
34
+ }
35
+ }
package/index.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { NexusChatHandle, NexusChatMount, NexusChatOptions } from './contract/index.js';
2
+
3
+ export type * from './contract/index.js';
4
+
5
+ export interface NexusChatSource {
6
+ /** IncortaCopilot's root as this page reaches it; the chat loads from `<copilotUrl>/nexus-chat/`. */
7
+ copilotUrl?: string;
8
+ /**
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.
11
+ */
12
+ assetsUrl?: string;
13
+ /** Deadline for loading the chat, retry included. Default 20 000 ms. */
14
+ timeoutMs?: number;
15
+ }
16
+
17
+ /** The host contract options; `version` is filled in for you. */
18
+ export type MountNexusChatOptions = Omit<NexusChatOptions, 'version'> &
19
+ Omit<NexusChatSource, 'copilotUrl'>;
20
+
21
+ /** The host contract shape this connector speaks. */
22
+ export const CONTRACT_VERSION: 2;
23
+
24
+ /** Loads the chat once per source (one retry, one deadline) and resolves to its `mount`. */
25
+ export function loadNexusChat(source: NexusChatSource): Promise<NexusChatMount>;
26
+
27
+ /** Loads the chat and mounts it into `element`. */
28
+ export function mountNexusChat(
29
+ element: HTMLElement,
30
+ options: MountNexusChatOptions,
31
+ ): Promise<NexusChatHandle>;
package/index.js ADDED
@@ -0,0 +1,87 @@
1
+ // @incorta/nexus-chat — the connector that loads and mounts NexusChat in any
2
+ // app, on any framework. The chat itself is a prebuilt Module Federation
3
+ // remote with its own React (`shared: {}`), loaded at runtime from one of:
4
+ //
5
+ // - IncortaCopilot (default): `<copilotUrl>/nexus-chat/`, always the version
6
+ // that Copilot runs, so the UI matches its backend;
7
+ // - this package's own `remote/` folder, served by the app (pinned to the
8
+ // installed version);
9
+ // - any URL that serves a `remote/` folder, such as a CDN (pinned by its URL).
10
+ //
11
+ // `assetsUrl` picks the source; without it the chat comes from Copilot.
12
+ // The host contract ships alongside: its types here, its values and helpers
13
+ // under `./contract`, `./conformance` and `./host`, and a React component
14
+ // under `./react`.
15
+
16
+ /** The host contract shape this connector speaks (`NEXUS_CHAT_CONTRACT_VERSION`). */
17
+ export const CONTRACT_VERSION = 2;
18
+
19
+ const RETRY_DELAY_MS = 1_000;
20
+ const DEFAULT_TIMEOUT_MS = 20_000;
21
+
22
+ const loads = new Map();
23
+ const attempts = new Map();
24
+
25
+ function entryUrl({ copilotUrl, assetsUrl }) {
26
+ const base = assetsUrl ?? (copilotUrl && `${copilotUrl.replace(/\/+$/, '')}/nexus-chat/`);
27
+ if (!base) throw new Error('NexusChat needs a copilotUrl or an assetsUrl.');
28
+ return new URL('remoteEntry.js', new URL(base, window.location.href)).href;
29
+ }
30
+
31
+ async function importMount(entry) {
32
+ // The browser remembers a failed import by URL, so every attempt after the
33
+ // first asks for a new one, or it would never reach the network again.
34
+ const attempt = attempts.get(entry) ?? 0;
35
+ attempts.set(entry, attempt + 1);
36
+ const url = new URL(entry);
37
+ if (attempt) url.searchParams.set('nexus-chat-attempt', String(attempt));
38
+ const remote = await import(/* @vite-ignore */ /* webpackIgnore: true */ url.href);
39
+ await remote.init({});
40
+ const factory = await remote.get('./mount');
41
+ const module = await factory();
42
+ if (typeof module?.default !== 'function') {
43
+ throw new Error('NexusChat remote did not expose a mount function.');
44
+ }
45
+ return module.default;
46
+ }
47
+
48
+ /**
49
+ * Loads the chat once per source and resolves to its `mount` function. One
50
+ * retry, since a single failure (a 502 during a rolling restart) is not an
51
+ * outage, and one deadline over both, so a stalled load never hangs. A failed
52
+ * load is forgotten: the next call tries again. Calling it early (for example
53
+ * when the page is idle) makes the first open instant.
54
+ */
55
+ export function loadNexusChat(options) {
56
+ const entry = entryUrl(options);
57
+ let load = loads.get(entry);
58
+ if (!load) {
59
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
60
+ load = Promise.race([
61
+ importMount(entry).catch(async () => {
62
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
63
+ return importMount(entry);
64
+ }),
65
+ new Promise((_, reject) =>
66
+ setTimeout(() => reject(new Error('Timed out loading NexusChat.')), timeoutMs),
67
+ ),
68
+ ]);
69
+ load.catch(() => loads.delete(entry));
70
+ loads.set(entry, load);
71
+ }
72
+ return load;
73
+ }
74
+
75
+ /**
76
+ * Loads the chat (from `assetsUrl`, else `connection.copilotUrl`) and mounts
77
+ * it into `element` with the host contract options. The chat fills the
78
+ * element, so the element's box decides the presentation.
79
+ */
80
+ export async function mountNexusChat(element, { assetsUrl, timeoutMs, ...options }) {
81
+ const mount = await loadNexusChat({
82
+ copilotUrl: options.connection?.copilotUrl,
83
+ assetsUrl,
84
+ timeoutMs,
85
+ });
86
+ return mount(element, { version: CONTRACT_VERSION, ...options });
87
+ }
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "@ahmedramez-org/nexus-chat",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0-beta.2",
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, and the chat bundle itself.",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Incorta/nexus-chat-ui.git",
9
+ "directory": "packages/nexus-chat"
10
+ },
11
+ "type": "module",
12
+ "sideEffects": false,
13
+ "exports": {
14
+ ".": {
15
+ "types": "./index.d.ts",
16
+ "default": "./index.js"
17
+ },
18
+ "./react": {
19
+ "types": "./react.d.ts",
20
+ "default": "./react.js"
21
+ },
22
+ "./contract": {
23
+ "types": "./contract/index.d.ts",
24
+ "default": "./contract/index.js"
25
+ },
26
+ "./conformance": {
27
+ "types": "./contract/conformance.d.ts",
28
+ "default": "./contract/conformance.js"
29
+ },
30
+ "./host": {
31
+ "types": "./contract/host.d.ts",
32
+ "default": "./contract/host.js"
33
+ },
34
+ "./remote/*": "./remote/*",
35
+ "./package.json": "./package.json"
36
+ },
37
+ "files": [
38
+ "index.js",
39
+ "index.d.ts",
40
+ "react.js",
41
+ "react.d.ts",
42
+ "contract",
43
+ "remote",
44
+ "README.md"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "peerDependencies": {
50
+ "react": ">=18"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "react": {
54
+ "optional": true
55
+ }
56
+ }
57
+ }
package/react.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ import type { CSSProperties, ReactElement } from 'react';
2
+
3
+ import type {
4
+ HostDirection,
5
+ HostTheme,
6
+ HostUser,
7
+ MountNexusChatOptions,
8
+ NexusChatHandle,
9
+ } from './index.js';
10
+
11
+ export interface NexusChatProps extends Omit<MountNexusChatOptions, 'environment' | 'window'> {
12
+ theme: HostTheme;
13
+ /** Default: the browser's language. */
14
+ locale?: string;
15
+ /** Absent: follows `locale`. */
16
+ direction?: HostDirection;
17
+ tenantId?: string;
18
+ user?: HostUser;
19
+ /**
20
+ * The app owns the chat's window (a docked panel, an overlay): the chat
21
+ * shows a dock toggle while `canDock`, and a close button.
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;
31
+ /** The chat could not load (after its retry and deadline). */
32
+ onLoadError?(error: Error): void;
33
+ className?: string;
34
+ style?: CSSProperties;
35
+ }
36
+
37
+ /**
38
+ * The chat as a React component. It fills its box. `connection`, `chat`,
39
+ * `actions`, `events`, `instanceId`, `assetsUrl` and `timeoutMs` are read
40
+ * once at mount; the other props update the running chat.
41
+ */
42
+ export function NexusChat(props: NexusChatProps): ReactElement;