@ahmedramez-org/nexus-chat 1.0.0-beta.16 → 1.0.0-beta.18

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.
@@ -267,14 +267,37 @@ export async function checkHostContract(options, checkOptions = {}) {
267
267
  });
268
268
  ensure(new Set(kinds).size === kinds.length, 'a context kind is listed twice (a source replaces its bare kind: list only the source).');
269
269
  });
270
- 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', () => {
271
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
+ }
272
277
  ensure(TRIGGER_CHARS.includes(trigger.char), `unknown trigger ${JSON.stringify(trigger.char)}.`);
273
278
  ensure(typeof trigger.search === 'function', `the ${trigger.char} trigger has no search().`);
274
279
  return trigger.char;
275
280
  });
276
281
  ensure(new Set(chars).size === chars.length, 'a trigger is listed twice.');
277
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
+ }
278
301
  await checks.run('actions', 'each action has can() and run()', () => {
279
302
  Object.entries(actions ?? {}).forEach(([name, action]) => {
280
303
  if (action === undefined)
@@ -133,15 +133,18 @@ export interface ValueMatch {
133
133
  dataType?: string;
134
134
  complete?: boolean;
135
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];
136
139
  /**
137
- * A composer trigger character. Both search inside the one attached business
138
- * view (`viewId` is that context's `id`); with none or several attached the
139
- * 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.
140
143
  *
141
144
  * - `@` mentions a column. Asked once per view; the chat filters as you type.
142
145
  * - `#` searches the view's values for the typed text.
143
146
  */
144
- export type TriggerSpec = {
147
+ export type TriggerSource = {
145
148
  char: '@';
146
149
  search(request: {
147
150
  viewId: string;
@@ -154,6 +157,21 @@ export type TriggerSpec = {
154
157
  limit: number;
155
158
  }, signal: AbortSignal): Promise<readonly ValueMatch[]>;
156
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;
157
175
  /** Switches for things the chat builds itself. Each one defaults to `true`. */
158
176
  export interface NexusChatFeatures {
159
177
  /**
@@ -274,6 +292,13 @@ export interface NexusChatChat {
274
292
  beforeSend?(request: {
275
293
  prompt: string;
276
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;
277
302
  }
278
303
  export type OpenIntent = {
279
304
  type: 'open-dashboard';
@@ -438,6 +463,13 @@ export interface NexusChatHandle {
438
463
  * gates: the agent must support the kind; `false` when refused).
439
464
  */
440
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;
441
473
  }
442
474
  /** The federated entry point `nexusChat/mount`. */
443
475
  export type NexusChatMount = (element: HTMLElement, options: NexusChatOptions) => NexusChatHandle;
package/contract/index.js CHANGED
@@ -11,6 +11,8 @@ export const BUILT_IN_CONTEXT_KINDS = [
11
11
  'ai-container',
12
12
  'knowledge-store',
13
13
  ];
14
+ /** The composer trigger characters the chat can serve itself. */
15
+ export const BUILT_IN_TRIGGERS = ['@', '#'];
14
16
  /**
15
17
  * Reads the namespaced-kind convention. Returns null for bare legacy kinds.
16
18
  * Framework-free on purpose: hosts use this inside `canRender` to route by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahmedramez-org/nexus-chat",
3
- "version": "1.0.0-beta.16",
3
+ "version": "1.0.0-beta.18",
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
@@ -304,7 +304,7 @@ function EmbeddedChat({ embedUrl, modes, defaultMode, theme = 'light', locale })
304
304
  ref: frame,
305
305
  title: NAME,
306
306
  src,
307
- allow: 'storage-access; microphone',
307
+ allow: 'storage-access; microphone; clipboard-write',
308
308
  style: { display: 'block', width: '100%', height: '100%', border: 0 },
309
309
  }),
310
310
  ),
package/window.js CHANGED
@@ -357,7 +357,7 @@ export function createEmbeddedChat({
357
357
  if (embedded || !win.opened) return;
358
358
  const iframe = document.createElement('iframe');
359
359
  iframe.title = NAME;
360
- iframe.allow = 'storage-access; microphone';
360
+ iframe.allow = 'storage-access; microphone; clipboard-write';
361
361
  iframe.src = embedSrc(embedUrl, theme, locale);
362
362
  iframe.style.cssText = 'display:block;width:100%;height:100%;border:0';
363
363
  win.panel.prepend(iframe);