@jupyter/chat 0.14.0 → 0.15.0

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.
@@ -118,7 +118,7 @@ export function ChatInput(props) {
118
118
  (!sendWithShiftEnter && !event.shiftKey)) {
119
119
  // Run all command providers
120
120
  await ((_a = props.chatCommandRegistry) === null || _a === void 0 ? void 0 : _a.onSubmit(model));
121
- model.send(input);
121
+ model.send(model.value);
122
122
  event.stopPropagation();
123
123
  event.preventDefault();
124
124
  }
@@ -14,7 +14,9 @@ import React, { useEffect, useState } from 'react';
14
14
  export function useChatCommands(inputModel, chatCommandRegistry) {
15
15
  // whether an option is highlighted in the chat commands menu
16
16
  const [highlighted, setHighlighted] = useState(false);
17
- // whether the chat commands menu is open
17
+ // whether the chat commands menu is open.
18
+ // NOTE: every `setOpen(false)` call should be followed by a
19
+ // `setHighlighted(false)` call.
18
20
  const [open, setOpen] = useState(false);
19
21
  // current list of chat commands matched by the current word.
20
22
  // the current word is the space-separated word at the user's cursor.
@@ -55,7 +57,13 @@ export function useChatCommands(inputModel, chatCommandRegistry) {
55
57
  }
56
58
  // Otherwise, open/close the menu based on the presence of command
57
59
  // completions and set the menu entries.
58
- setOpen(!!commandCompletions.length);
60
+ if (commandCompletions.length) {
61
+ setOpen(true);
62
+ }
63
+ else {
64
+ setOpen(false);
65
+ setHighlighted(false);
66
+ }
59
67
  setCommands(commandCompletions);
60
68
  }
61
69
  inputModel.currentWordChanged.connect(getCommands);
@@ -91,7 +99,7 @@ export function useChatCommands(inputModel, chatCommandRegistry) {
91
99
  autocompleteProps: {
92
100
  open,
93
101
  options: commands,
94
- getOptionLabel: (command) => command.name,
102
+ getOptionLabel: (command) => typeof command === 'string' ? '' : command.name,
95
103
  renderOption: (defaultProps, command, __, ___) => {
96
104
  const { key, ...listItemProps } = defaultProps;
97
105
  const commandIcon = React.isValidElement(command.icon) ? (command.icon) : (React.createElement("span", null, command.icon instanceof LabIcon ? (React.createElement(command.icon.react, null)) : (command.icon)));
@@ -3,7 +3,6 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
  import { Signal } from '@lumino/signaling';
6
- const WHITESPACE = new Set([' ', '\n', '\t']);
7
6
  /**
8
7
  * The input model.
9
8
  */
@@ -254,14 +253,42 @@ export class InputModel {
254
253
  }
255
254
  var Private;
256
255
  (function (Private) {
256
+ const WHITESPACE = new Set([' ', '\n', '\t']);
257
+ /**
258
+ * Returns the start index (inclusive) & end index (exclusive) that contain
259
+ * the current word. The start & end index can be passed to `String.slice()`
260
+ * to extract the current word. The returned range never includes any
261
+ * whitespace character unless it is escaped by a backslash `\`.
262
+ *
263
+ * NOTE: the escape sequence handling here is naive and non-recursive. This
264
+ * function considers the space in "`\\ `" as escaped, even though "`\\ `"
265
+ * defines a backslash followed by an _unescaped_ space in most languages.
266
+ */
257
267
  function getCurrentWordBoundaries(input, cursorIndex) {
258
268
  let start = cursorIndex;
259
269
  let end = cursorIndex;
260
270
  const n = input.length;
261
- while (start > 0 && !WHITESPACE.has(input[start - 1])) {
271
+ while (
272
+ // terminate when `input[start - 1]` is whitespace
273
+ // i.e. `input[start]` is never whitespace after exiting
274
+ (start - 1 >= 0 && !WHITESPACE.has(input[start - 1])) ||
275
+ // unless it is preceded by a backslash
276
+ (start - 2 >= 0 &&
277
+ input[start - 2] === '\\' &&
278
+ WHITESPACE.has(input[start - 1]))) {
262
279
  start--;
263
280
  }
264
- while (end < n && !WHITESPACE.has(input[end])) {
281
+ // `end` is an exclusive index unlike `start`, hence the different `while`
282
+ // condition here
283
+ while (
284
+ // terminate when `input[end]` is whitespace
285
+ // i.e. `input[end]` may be whitespace after exiting
286
+ (end < n && !WHITESPACE.has(input[end])) ||
287
+ // unless it is preceded by a backslash
288
+ (end < n &&
289
+ end - 1 >= 0 &&
290
+ input[end - 1] === '\\' &&
291
+ WHITESPACE.has(input[end]))) {
265
292
  end++;
266
293
  }
267
294
  return [start, end];
package/lib/types.d.ts CHANGED
@@ -9,9 +9,14 @@ export interface IUser {
9
9
  color?: string;
10
10
  avatar_url?: string;
11
11
  /**
12
- * The string to use to mention a user in the chat.
12
+ * The string to use to mention a user in the chat. This is computed via the
13
+ * following procedure:
14
+ *
15
+ * 1. Let `mention_name = user.display_name || user.name || user.username`.
16
+ *
17
+ * 2. Replace each ' ' character with '-' in `mention_name`.
13
18
  */
14
- mention_name?: string;
19
+ mention_name: string;
15
20
  /**
16
21
  * Boolean identifying if user is a bot.
17
22
  */
package/lib/utils.js CHANGED
@@ -46,9 +46,10 @@ export function replaceMentionToSpan(content, user) {
46
46
  if (!user.mention_name) {
47
47
  return content;
48
48
  }
49
- const regex = new RegExp(user.mention_name, 'g');
50
- const mention = `<span class="${MENTION_CLASS}">${user.mention_name}</span>`;
51
- return content.replace(regex, mention);
49
+ const mention = '@' + user.mention_name;
50
+ const regex = new RegExp(mention, 'g');
51
+ const mentionEl = `<span class="${MENTION_CLASS}">${mention}</span>`;
52
+ return content.replace(regex, mentionEl);
52
53
  }
53
54
  /**
54
55
  * Replace a span to a mentioned to user string (@someone).
@@ -60,7 +61,8 @@ export function replaceSpanToMention(content, user) {
60
61
  if (!user.mention_name) {
61
62
  return content;
62
63
  }
63
- const span = `<span class="${MENTION_CLASS}">${user.mention_name}</span>`;
64
- const regex = new RegExp(span, 'g');
65
- return content.replace(regex, user.mention_name);
64
+ const mention = '@' + user.mention_name;
65
+ const mentionEl = `<span class="${MENTION_CLASS}">${mention}</span>`;
66
+ const regex = new RegExp(mentionEl, 'g');
67
+ return content.replace(regex, mention);
66
68
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyter/chat",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "A package that provides UI components that can be used to create a chat in a Jupyterlab extension.",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -159,7 +159,7 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
159
159
  ) {
160
160
  // Run all command providers
161
161
  await props.chatCommandRegistry?.onSubmit(model);
162
- model.send(input);
162
+ model.send(model.value);
163
163
  event.stopPropagation();
164
164
  event.preventDefault();
165
165
  }
@@ -37,7 +37,9 @@ export function useChatCommands(
37
37
  // whether an option is highlighted in the chat commands menu
38
38
  const [highlighted, setHighlighted] = useState(false);
39
39
 
40
- // whether the chat commands menu is open
40
+ // whether the chat commands menu is open.
41
+ // NOTE: every `setOpen(false)` call should be followed by a
42
+ // `setHighlighted(false)` call.
41
43
  const [open, setOpen] = useState(false);
42
44
 
43
45
  // current list of chat commands matched by the current word.
@@ -90,7 +92,12 @@ export function useChatCommands(
90
92
 
91
93
  // Otherwise, open/close the menu based on the presence of command
92
94
  // completions and set the menu entries.
93
- setOpen(!!commandCompletions.length);
95
+ if (commandCompletions.length) {
96
+ setOpen(true);
97
+ } else {
98
+ setOpen(false);
99
+ setHighlighted(false);
100
+ }
94
101
  setCommands(commandCompletions);
95
102
  }
96
103
 
@@ -138,7 +145,8 @@ export function useChatCommands(
138
145
  autocompleteProps: {
139
146
  open,
140
147
  options: commands,
141
- getOptionLabel: (command: ChatCommand) => command.name,
148
+ getOptionLabel: (command: ChatCommand | string) =>
149
+ typeof command === 'string' ? '' : command.name,
142
150
  renderOption: (
143
151
  defaultProps,
144
152
  command: ChatCommand,
@@ -11,8 +11,6 @@ import { ISelectionWatcher } from './selection-watcher';
11
11
  import { IChatContext } from './model';
12
12
  import { IAttachment, IUser } from './types';
13
13
 
14
- const WHITESPACE = new Set([' ', '\n', '\t']);
15
-
16
14
  /**
17
15
  * The chat input interface.
18
16
  */
@@ -525,6 +523,18 @@ export namespace InputModel {
525
523
  }
526
524
 
527
525
  namespace Private {
526
+ const WHITESPACE = new Set([' ', '\n', '\t']);
527
+
528
+ /**
529
+ * Returns the start index (inclusive) & end index (exclusive) that contain
530
+ * the current word. The start & end index can be passed to `String.slice()`
531
+ * to extract the current word. The returned range never includes any
532
+ * whitespace character unless it is escaped by a backslash `\`.
533
+ *
534
+ * NOTE: the escape sequence handling here is naive and non-recursive. This
535
+ * function considers the space in "`\\ `" as escaped, even though "`\\ `"
536
+ * defines a backslash followed by an _unescaped_ space in most languages.
537
+ */
528
538
  export function getCurrentWordBoundaries(
529
539
  input: string,
530
540
  cursorIndex: number
@@ -533,11 +543,30 @@ namespace Private {
533
543
  let end = cursorIndex;
534
544
  const n = input.length;
535
545
 
536
- while (start > 0 && !WHITESPACE.has(input[start - 1])) {
546
+ while (
547
+ // terminate when `input[start - 1]` is whitespace
548
+ // i.e. `input[start]` is never whitespace after exiting
549
+ (start - 1 >= 0 && !WHITESPACE.has(input[start - 1])) ||
550
+ // unless it is preceded by a backslash
551
+ (start - 2 >= 0 &&
552
+ input[start - 2] === '\\' &&
553
+ WHITESPACE.has(input[start - 1]))
554
+ ) {
537
555
  start--;
538
556
  }
539
557
 
540
- while (end < n && !WHITESPACE.has(input[end])) {
558
+ // `end` is an exclusive index unlike `start`, hence the different `while`
559
+ // condition here
560
+ while (
561
+ // terminate when `input[end]` is whitespace
562
+ // i.e. `input[end]` may be whitespace after exiting
563
+ (end < n && !WHITESPACE.has(input[end])) ||
564
+ // unless it is preceded by a backslash
565
+ (end < n &&
566
+ end - 1 >= 0 &&
567
+ input[end - 1] === '\\' &&
568
+ WHITESPACE.has(input[end]))
569
+ ) {
541
570
  end++;
542
571
  }
543
572
 
package/src/types.ts CHANGED
@@ -14,9 +14,14 @@ export interface IUser {
14
14
  color?: string;
15
15
  avatar_url?: string;
16
16
  /**
17
- * The string to use to mention a user in the chat.
17
+ * The string to use to mention a user in the chat. This is computed via the
18
+ * following procedure:
19
+ *
20
+ * 1. Let `mention_name = user.display_name || user.name || user.username`.
21
+ *
22
+ * 2. Replace each ' ' character with '-' in `mention_name`.
18
23
  */
19
- mention_name?: string;
24
+ mention_name: string;
20
25
  /**
21
26
  * Boolean identifying if user is a bot.
22
27
  */
package/src/utils.ts CHANGED
@@ -60,9 +60,10 @@ export function replaceMentionToSpan(content: string, user: IUser): string {
60
60
  if (!user.mention_name) {
61
61
  return content;
62
62
  }
63
- const regex = new RegExp(user.mention_name, 'g');
64
- const mention = `<span class="${MENTION_CLASS}">${user.mention_name}</span>`;
65
- return content.replace(regex, mention);
63
+ const mention = '@' + user.mention_name;
64
+ const regex = new RegExp(mention, 'g');
65
+ const mentionEl = `<span class="${MENTION_CLASS}">${mention}</span>`;
66
+ return content.replace(regex, mentionEl);
66
67
  }
67
68
 
68
69
  /**
@@ -75,7 +76,8 @@ export function replaceSpanToMention(content: string, user: IUser): string {
75
76
  if (!user.mention_name) {
76
77
  return content;
77
78
  }
78
- const span = `<span class="${MENTION_CLASS}">${user.mention_name}</span>`;
79
- const regex = new RegExp(span, 'g');
80
- return content.replace(regex, user.mention_name);
79
+ const mention = '@' + user.mention_name;
80
+ const mentionEl = `<span class="${MENTION_CLASS}">${mention}</span>`;
81
+ const regex = new RegExp(mentionEl, 'g');
82
+ return content.replace(regex, mention);
81
83
  }