@jupyter/chat 0.8.1 → 0.10.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.
Files changed (69) hide show
  1. package/lib/__tests__/mocks.d.ts +9 -0
  2. package/lib/__tests__/mocks.js +18 -0
  3. package/lib/__tests__/model.spec.js +17 -10
  4. package/lib/__tests__/widgets.spec.js +4 -4
  5. package/lib/chat-commands/types.d.ts +2 -1
  6. package/lib/components/chat-input.d.ts +4 -12
  7. package/lib/components/chat-input.js +26 -40
  8. package/lib/components/chat-messages.d.ts +17 -4
  9. package/lib/components/chat-messages.js +28 -15
  10. package/lib/components/chat.d.ts +5 -5
  11. package/lib/components/chat.js +9 -8
  12. package/lib/components/code-blocks/copy-button.js +6 -3
  13. package/lib/components/input/buttons/attach-button.d.ts +6 -0
  14. package/lib/components/input/{attach-button.js → buttons/attach-button.js} +11 -8
  15. package/lib/components/input/buttons/cancel-button.d.ts +6 -0
  16. package/lib/components/input/{cancel-button.js → buttons/cancel-button.js} +5 -7
  17. package/lib/components/input/buttons/index.d.ts +3 -0
  18. package/lib/components/input/buttons/index.js +7 -0
  19. package/lib/components/input/buttons/send-button.d.ts +6 -0
  20. package/lib/components/input/{send-button.js → buttons/send-button.js} +52 -42
  21. package/lib/components/input/index.d.ts +3 -3
  22. package/lib/components/input/index.js +3 -3
  23. package/lib/components/input/toolbar-registry.d.ts +98 -0
  24. package/lib/components/input/toolbar-registry.js +85 -0
  25. package/lib/components/input/use-chat-commands.js +6 -5
  26. package/lib/components/mui-extras/tooltipped-button.d.ts +1 -1
  27. package/lib/components/mui-extras/tooltipped-button.js +3 -2
  28. package/lib/components/mui-extras/tooltipped-icon-button.js +4 -2
  29. package/lib/index.d.ts +1 -1
  30. package/lib/index.js +1 -1
  31. package/lib/input-model.d.ts +93 -1
  32. package/lib/input-model.js +55 -1
  33. package/lib/model.d.ts +76 -9
  34. package/lib/model.js +42 -12
  35. package/lib/types.d.ts +5 -18
  36. package/lib/utils.d.ts +15 -0
  37. package/lib/utils.js +29 -0
  38. package/lib/widgets/chat-widget.d.ts +5 -1
  39. package/lib/widgets/chat-widget.js +7 -1
  40. package/package.json +1 -1
  41. package/src/__tests__/mocks.ts +31 -0
  42. package/src/__tests__/model.spec.ts +21 -11
  43. package/src/__tests__/widgets.spec.ts +5 -4
  44. package/src/chat-commands/types.ts +1 -1
  45. package/src/components/chat-input.tsx +41 -66
  46. package/src/components/chat-messages.tsx +44 -17
  47. package/src/components/chat.tsx +12 -21
  48. package/src/components/code-blocks/copy-button.tsx +9 -3
  49. package/src/components/input/{attach-button.tsx → buttons/attach-button.tsx} +15 -20
  50. package/src/components/input/{cancel-button.tsx → buttons/cancel-button.tsx} +9 -16
  51. package/src/components/input/buttons/index.ts +8 -0
  52. package/src/components/input/{send-button.tsx → buttons/send-button.tsx} +62 -61
  53. package/src/components/input/index.ts +3 -3
  54. package/src/components/input/toolbar-registry.tsx +162 -0
  55. package/src/components/input/use-chat-commands.tsx +14 -6
  56. package/src/components/mui-extras/tooltipped-button.tsx +4 -2
  57. package/src/components/mui-extras/tooltipped-icon-button.tsx +5 -2
  58. package/src/index.ts +1 -1
  59. package/src/input-model.ts +140 -2
  60. package/src/model.ts +110 -12
  61. package/src/types.ts +5 -21
  62. package/src/utils.ts +34 -0
  63. package/src/widgets/chat-widget.tsx +8 -1
  64. package/style/base.css +1 -0
  65. package/style/chat.css +6 -0
  66. package/style/input.css +32 -0
  67. package/lib/components/input/attach-button.d.ts +0 -14
  68. package/lib/components/input/cancel-button.d.ts +0 -11
  69. package/lib/components/input/send-button.d.ts +0 -18
@@ -8,36 +8,29 @@ import SendIcon from '@mui/icons-material/Send';
8
8
  import { Box, Menu, MenuItem, Typography } from '@mui/material';
9
9
  import React, { useCallback, useEffect, useState } from 'react';
10
10
 
11
- import { TooltippedButton } from '../mui-extras/tooltipped-button';
12
- import { includeSelectionIcon } from '../../icons';
13
- import { IInputModel } from '../../input-model';
14
- import { Selection } from '../../types';
11
+ import { InputToolbarRegistry } from '../toolbar-registry';
12
+ import { TooltippedButton } from '../../mui-extras/tooltipped-button';
13
+ import { includeSelectionIcon } from '../../../icons';
14
+ import { IInputModel, InputModel } from '../../../input-model';
15
15
 
16
16
  const SEND_BUTTON_CLASS = 'jp-chat-send-button';
17
17
  const SEND_INCLUDE_OPENER_CLASS = 'jp-chat-send-include-opener';
18
18
  const SEND_INCLUDE_LI_CLASS = 'jp-chat-send-include';
19
19
 
20
- /**
21
- * The send button props.
22
- */
23
- export type SendButtonProps = {
24
- model: IInputModel;
25
- sendWithShiftEnter: boolean;
26
- inputExists: boolean;
27
- onSend: (selection?: Selection) => unknown;
28
- hideIncludeSelection?: boolean;
29
- hasButtonOnLeft?: boolean;
30
- };
31
-
32
20
  /**
33
21
  * The send button, with optional 'include selection' menu.
34
22
  */
35
- export function SendButton(props: SendButtonProps): JSX.Element {
36
- const { activeCellManager, selectionWatcher } = props.model;
37
- const hideIncludeSelection = props.hideIncludeSelection ?? false;
38
- const hasButtonOnLeft = props.hasButtonOnLeft ?? false;
23
+ export function SendButton(
24
+ props: InputToolbarRegistry.IToolbarItemProps
25
+ ): JSX.Element {
26
+ const { model } = props;
27
+ const { activeCellManager, selectionWatcher } = model;
28
+ const hideIncludeSelection = !activeCellManager || !selectionWatcher;
29
+
39
30
  const [menuAnchorEl, setMenuAnchorEl] = useState<HTMLElement | null>(null);
40
31
  const [menuOpen, setMenuOpen] = useState(false);
32
+ const [disabled, setDisabled] = useState(false);
33
+ const [tooltip, setTooltip] = useState<string>('');
41
34
 
42
35
  const openMenu = useCallback((el: HTMLElement | null) => {
43
36
  setMenuAnchorEl(el);
@@ -48,11 +41,36 @@ export function SendButton(props: SendButtonProps): JSX.Element {
48
41
  setMenuOpen(false);
49
42
  }, []);
50
43
 
51
- const disabled = !props.inputExists;
52
-
53
44
  const [selectionTooltip, setSelectionTooltip] = useState<string>('');
54
45
  const [disableInclude, setDisableInclude] = useState<boolean>(true);
55
46
 
47
+ useEffect(() => {
48
+ const inputChanged = () => {
49
+ const inputExist = !!model.value.trim() || model.attachments.length;
50
+ setDisabled(!inputExist);
51
+ };
52
+
53
+ model.valueChanged.connect(inputChanged);
54
+ model.attachmentsChanged?.connect(inputChanged);
55
+
56
+ inputChanged();
57
+
58
+ const configChanged = (_: IInputModel, config: InputModel.IConfig) => {
59
+ setTooltip(
60
+ (config.sendWithShiftEnter ?? false)
61
+ ? 'Send message (SHIFT+ENTER)'
62
+ : 'Send message (ENTER)'
63
+ );
64
+ };
65
+ model.configChanged.connect(configChanged);
66
+
67
+ return () => {
68
+ model.valueChanged.disconnect(inputChanged);
69
+ model.attachmentsChanged?.disconnect(inputChanged);
70
+ model.configChanged?.disconnect(configChanged);
71
+ };
72
+ }, [model]);
73
+
56
74
  useEffect(() => {
57
75
  /**
58
76
  * Enable or disable the include selection button, and adapt the tooltip.
@@ -78,55 +96,44 @@ export function SendButton(props: SendButtonProps): JSX.Element {
78
96
  selectionWatcher?.selectionChanged.disconnect(toggleIncludeState);
79
97
  activeCellManager?.availabilityChanged.disconnect(toggleIncludeState);
80
98
  };
81
- }, [activeCellManager, selectionWatcher, hideIncludeSelection]);
82
-
83
- const defaultTooltip = props.sendWithShiftEnter
84
- ? 'Send message (SHIFT+ENTER)'
85
- : 'Send message (ENTER)';
86
- const tooltip = defaultTooltip;
99
+ }, [activeCellManager, selectionWatcher]);
87
100
 
88
101
  function sendWithSelection() {
89
- // Append the selected text if exists.
102
+ let source = '';
103
+
90
104
  if (selectionWatcher?.selection) {
91
- props.onSend({
92
- type: 'text',
93
- source: selectionWatcher.selection.text
94
- });
95
- closeMenu();
96
- return;
105
+ // Append the selected text if exists.
106
+ source = selectionWatcher.selection.text;
107
+ } else if (activeCellManager?.available) {
108
+ // Append the active cell content if exists.
109
+ source = activeCellManager.getContent(false)!.source;
97
110
  }
111
+ let content = model.value;
112
+ if (source) {
113
+ content += `
98
114
 
99
- // Append the active cell content if exists.
100
- if (activeCellManager?.available) {
101
- props.onSend({
102
- type: 'cell',
103
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
104
- source: activeCellManager.getContent(false)!.source
105
- });
106
- closeMenu();
107
- return;
115
+ \`\`\`
116
+ ${source}
117
+ \`\`\`
118
+ `;
108
119
  }
120
+ model.send(content);
121
+ closeMenu();
122
+ model.value = '';
109
123
  }
110
124
 
111
125
  return (
112
- <Box sx={{ display: 'flex', flexWrap: 'nowrap' }}>
126
+ <>
113
127
  <TooltippedButton
114
- onClick={() => props.onSend()}
128
+ onClick={() => model.send(model.value)}
115
129
  disabled={disabled}
116
130
  tooltip={tooltip}
117
131
  buttonProps={{
118
132
  size: 'small',
119
- title: defaultTooltip,
133
+ title: tooltip,
120
134
  variant: 'contained',
121
135
  className: SEND_BUTTON_CLASS
122
136
  }}
123
- sx={{
124
- minWidth: 'unset',
125
- borderTopLeftRadius: hasButtonOnLeft ? '0px' : '2px',
126
- borderTopRightRadius: hideIncludeSelection ? '2px' : '0px',
127
- borderBottomRightRadius: hideIncludeSelection ? '2px' : '0px',
128
- borderBottomLeftRadius: hasButtonOnLeft ? '0px' : '2px'
129
- }}
130
137
  >
131
138
  <SendIcon />
132
139
  </TooltippedButton>
@@ -151,12 +158,6 @@ export function SendButton(props: SendButtonProps): JSX.Element {
151
158
  },
152
159
  className: SEND_INCLUDE_OPENER_CLASS
153
160
  }}
154
- sx={{
155
- minWidth: 'unset',
156
- padding: '4px 0px',
157
- borderRadius: '0px 2px 2px 0px',
158
- marginLeft: '1px'
159
- }}
160
161
  >
161
162
  <KeyboardArrowDown />
162
163
  </TooltippedButton>
@@ -205,6 +206,6 @@ export function SendButton(props: SendButtonProps): JSX.Element {
205
206
  </Menu>
206
207
  </>
207
208
  )}
208
- </Box>
209
+ </>
209
210
  );
210
211
  }
@@ -3,6 +3,6 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
- export * from './attach-button';
7
- export * from './cancel-button';
8
- export * from './send-button';
6
+ export * from './buttons';
7
+ export * from './toolbar-registry';
8
+ export * from './use-chat-commands';
@@ -0,0 +1,162 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import * as React from 'react';
6
+
7
+ import { AttachButton, CancelButton, SendButton } from './buttons';
8
+ import { IInputModel } from '../../input-model';
9
+ import { ISignal, Signal } from '@lumino/signaling';
10
+
11
+ /**
12
+ * The toolbar registry interface.
13
+ */
14
+ export interface IInputToolbarRegistry {
15
+ /**
16
+ * A signal emitting when the items has changed.
17
+ */
18
+ readonly itemsChanged: ISignal<IInputToolbarRegistry, void>;
19
+ /**
20
+ * Get a toolbar item.
21
+ */
22
+ get(name: string): InputToolbarRegistry.IToolbarItem | undefined;
23
+
24
+ /**
25
+ * Get the list of the visible toolbar items in order.
26
+ */
27
+ getItems(): InputToolbarRegistry.IToolbarItem[];
28
+
29
+ /**
30
+ * Add a toolbar item.
31
+ */
32
+ addItem(name: string, item: InputToolbarRegistry.IToolbarItem): void;
33
+
34
+ /**
35
+ * Hide an element.
36
+ */
37
+ hide(name: string): void;
38
+
39
+ /**
40
+ * Show an element.
41
+ */
42
+ show(name: string): void;
43
+ }
44
+
45
+ /**
46
+ * The toolbar registry implementation.
47
+ */
48
+ export class InputToolbarRegistry implements IInputToolbarRegistry {
49
+ /**
50
+ * A signal emitting when the items has changed.
51
+ */
52
+ get itemsChanged(): ISignal<IInputToolbarRegistry, void> {
53
+ return this._itemsChanged;
54
+ }
55
+
56
+ /**
57
+ * Get a toolbar item.
58
+ */
59
+ get(name: string): InputToolbarRegistry.IToolbarItem | undefined {
60
+ return this._items.get(name);
61
+ }
62
+
63
+ /**
64
+ * Get the list of the visible toolbar items in order.
65
+ */
66
+ getItems(): InputToolbarRegistry.IToolbarItem[] {
67
+ return Array.from(this._items.values())
68
+ .filter(item => !item.hidden)
69
+ .sort((a, b) => a.position - b.position);
70
+ }
71
+
72
+ /**
73
+ * Add a toolbar item.
74
+ */
75
+ addItem(name: string, item: InputToolbarRegistry.IToolbarItem): void {
76
+ if (!this._items.has(name)) {
77
+ this._items.set(name, item);
78
+ this._itemsChanged.emit();
79
+ } else {
80
+ console.warn(`A chat input toolbar item '${name}' is already registered`);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Hide an element.
86
+ */
87
+ hide(name: string): void {
88
+ const item = this._items.get(name);
89
+ if (item) {
90
+ item.hidden = true;
91
+ this._itemsChanged.emit();
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Show an element.
97
+ */
98
+ show(name: string): void {
99
+ const item = this._items.get(name);
100
+ if (item) {
101
+ item.hidden = false;
102
+ this._itemsChanged.emit();
103
+ }
104
+ }
105
+
106
+ private _items = new Map<string, InputToolbarRegistry.IToolbarItem>();
107
+ private _itemsChanged = new Signal<this, void>(this);
108
+ }
109
+
110
+ export namespace InputToolbarRegistry {
111
+ /**
112
+ * The toolbar item interface.
113
+ */
114
+ export interface IToolbarItem {
115
+ /**
116
+ * The react functional component with the button.
117
+ *
118
+ * NOTE:
119
+ * This component must be a TooltippedButton for a good integration in the toolbar.
120
+ */
121
+ element: React.FunctionComponent<IToolbarItemProps>;
122
+ /**
123
+ * The position of the button in the toolbar.
124
+ */
125
+ position: number;
126
+ /**
127
+ * Whether the button is hidden or not.
128
+ */
129
+ hidden?: boolean;
130
+ }
131
+
132
+ /**
133
+ * The toolbar item properties, send to the button.
134
+ */
135
+ export interface IToolbarItemProps {
136
+ /**
137
+ * The input model of the input component including the button.
138
+ */
139
+ model: IInputModel;
140
+ }
141
+
142
+ /**
143
+ * The default toolbar registry if none is provided.
144
+ */
145
+ export function defaultToolbarRegistry(): InputToolbarRegistry {
146
+ const registry = new InputToolbarRegistry();
147
+
148
+ registry.addItem('send', {
149
+ element: SendButton,
150
+ position: 100
151
+ });
152
+ registry.addItem('attach', {
153
+ element: AttachButton,
154
+ position: 20
155
+ });
156
+ registry.addItem('cancel', {
157
+ element: CancelButton,
158
+ position: 10
159
+ });
160
+ return registry;
161
+ }
162
+ }
@@ -3,13 +3,13 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
- import React from 'react';
7
- import { useEffect, useState } from 'react';
6
+ import { LabIcon } from '@jupyterlab/ui-components';
8
7
  import type {
9
8
  AutocompleteChangeReason,
10
9
  AutocompleteProps as GenericAutocompleteProps
11
10
  } from '@mui/material';
12
11
  import { Box } from '@mui/material';
12
+ import React, { useEffect, useState } from 'react';
13
13
 
14
14
  import { ChatCommand, IChatCommandRegistry } from '../../chat-commands';
15
15
  import { IInputModel } from '../../input-model';
@@ -131,9 +131,11 @@ export function useChatCommands(
131
131
  ___: unknown
132
132
  ) => {
133
133
  const { key, ...listItemProps } = defaultProps;
134
- const commandIcon: JSX.Element = (
134
+ const commandIcon: JSX.Element = React.isValidElement(command.icon) ? (
135
+ command.icon
136
+ ) : (
135
137
  <span>
136
- {typeof command.icon === 'object' ? (
138
+ {command.icon instanceof LabIcon ? (
137
139
  <command.icon.react />
138
140
  ) : (
139
141
  command.icon
@@ -144,8 +146,14 @@ export function useChatCommands(
144
146
  <Box key={key} component="li" {...listItemProps}>
145
147
  {commandIcon}
146
148
  <p className="jp-chat-command-name">{command.name}</p>
147
- <span> - </span>
148
- <p className="jp-chat-command-description">{command.description}</p>
149
+ {command.description && (
150
+ <>
151
+ <span> - </span>
152
+ <p className="jp-chat-command-description">
153
+ {command.description}
154
+ </p>
155
+ </>
156
+ )}
149
157
  </Box>
150
158
  );
151
159
  },
@@ -3,11 +3,13 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
- import React from 'react';
7
6
  import { Button, ButtonProps, SxProps, TooltipProps } from '@mui/material';
7
+ import React from 'react';
8
8
 
9
9
  import { ContrastingTooltip } from './contrasting-tooltip';
10
10
 
11
+ const TOOLTIPPED_WRAP_CLASS = 'jp-chat-tooltipped-wrap';
12
+
11
13
  export type TooltippedButtonProps = {
12
14
  onClick: React.MouseEventHandler<HTMLButtonElement>;
13
15
  tooltip: string;
@@ -72,7 +74,7 @@ export function TooltippedButton(props: TooltippedButtonProps): JSX.Element {
72
74
 
73
75
  See: https://mui.com/material-ui/react-tooltip/#disabled-elements
74
76
  */}
75
- <span style={{ cursor: 'default' }}>
77
+ <span style={{ cursor: 'default' }} className={TOOLTIPPED_WRAP_CLASS}>
76
78
  <Button
77
79
  {...props.buttonProps}
78
80
  onClick={props.onClick}
@@ -3,11 +3,14 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
- import React from 'react';
6
+ import { classes } from '@jupyterlab/ui-components';
7
7
  import { IconButton, IconButtonProps, TooltipProps } from '@mui/material';
8
+ import React from 'react';
8
9
 
9
10
  import { ContrastingTooltip } from './contrasting-tooltip';
10
11
 
12
+ const TOOLTIPPED_WRAP_CLASS = 'jp-chat-tooltipped-wrap';
13
+
11
14
  export type TooltippedIconButtonProps = {
12
15
  onClick: () => unknown;
13
16
  tooltip: string;
@@ -68,7 +71,7 @@ export function TooltippedIconButton(
68
71
 
69
72
  See: https://mui.com/material-ui/react-tooltip/#disabled-elements
70
73
  */}
71
- <span className={props.className}>
74
+ <span className={classes(props.className, TOOLTIPPED_WRAP_CLASS)}>
72
75
  <IconButton
73
76
  {...props.iconButtonProps}
74
77
  onClick={props.onClick}
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  export * from './active-cell-manager';
7
+ export * from './chat-commands';
7
8
  export * from './components';
8
9
  export * from './icons';
9
10
  export * from './input-model';
@@ -14,4 +15,3 @@ export * from './types';
14
15
  export * from './widgets/chat-error';
15
16
  export * from './widgets/chat-sidebar';
16
17
  export * from './widgets/chat-widget';
17
- export * from './chat-commands';
@@ -3,11 +3,13 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
+ import { IDocumentManager } from '@jupyterlab/docmanager';
6
7
  import { IDisposable } from '@lumino/disposable';
7
8
  import { ISignal, Signal } from '@lumino/signaling';
8
9
  import { IActiveCellManager } from './active-cell-manager';
9
10
  import { ISelectionWatcher } from './selection-watcher';
10
- import { IAttachment } from './types';
11
+ import { IChatContext } from './model';
12
+ import { IAttachment, IUser } from './types';
11
13
 
12
14
  const WHITESPACE = new Set([' ', '\n', '\t']);
13
15
 
@@ -15,6 +17,21 @@ const WHITESPACE = new Set([' ', '\n', '\t']);
15
17
  * The chat input interface.
16
18
  */
17
19
  export interface IInputModel extends IDisposable {
20
+ /**
21
+ * The chat context (a readonly subset of the chat model).
22
+ */
23
+ readonly chatContext: IChatContext;
24
+
25
+ /**
26
+ * Function to send a message.
27
+ */
28
+ send: (content: string) => void;
29
+
30
+ /**
31
+ * Optional function to cancel edition.
32
+ */
33
+ cancel: (() => void) | undefined;
34
+
18
35
  /**
19
36
  * The entire input value.
20
37
  */
@@ -56,6 +73,11 @@ export interface IInputModel extends IDisposable {
56
73
  */
57
74
  readonly selectionWatcher: ISelectionWatcher | null;
58
75
 
76
+ /**
77
+ * Get the document manager.
78
+ */
79
+ readonly documentManager: IDocumentManager | null;
80
+
59
81
  /**
60
82
  * The input configuration.
61
83
  */
@@ -105,6 +127,26 @@ export interface IInputModel extends IDisposable {
105
127
  * Replace the current word in the input with a new one.
106
128
  */
107
129
  replaceCurrentWord(newWord: string): void;
130
+
131
+ /**
132
+ * The mentioned user list.
133
+ */
134
+ readonly mentions: IUser[];
135
+
136
+ /**
137
+ * Add user mention.
138
+ */
139
+ addMention?(user: IUser): void;
140
+
141
+ /**
142
+ * Remove a user mention.
143
+ */
144
+ removeMention(user: IUser): void;
145
+
146
+ /**
147
+ * Clear mentions list.
148
+ */
149
+ clearMentions(): void;
108
150
  }
109
151
 
110
152
  /**
@@ -112,17 +154,41 @@ export interface IInputModel extends IDisposable {
112
154
  */
113
155
  export class InputModel implements IInputModel {
114
156
  constructor(options: InputModel.IOptions) {
157
+ this._onSend = options.onSend;
158
+ this._chatContext = options.chatContext;
115
159
  this._value = options.value || '';
116
160
  this._attachments = options.attachments || [];
161
+ this._mentions = options.mentions || [];
117
162
  this.cursorIndex = options.cursorIndex || this.value.length;
118
163
  this._activeCellManager = options.activeCellManager ?? null;
119
164
  this._selectionWatcher = options.selectionWatcher ?? null;
120
-
165
+ this._documentManager = options.documentManager ?? null;
121
166
  this._config = {
122
167
  ...options.config
123
168
  };
169
+ this.cancel = options.onCancel;
170
+ }
171
+
172
+ /**
173
+ * The chat context (a readonly subset of the chat model);
174
+ */
175
+ get chatContext(): IChatContext {
176
+ return this._chatContext;
124
177
  }
125
178
 
179
+ /**
180
+ * Function to send a message.
181
+ */
182
+ send = (input: string): void => {
183
+ this._onSend(input, this);
184
+ this.value = '';
185
+ };
186
+
187
+ /**
188
+ * Optional function to cancel edition.
189
+ */
190
+ cancel: (() => void) | undefined;
191
+
126
192
  /**
127
193
  * The entire input value.
128
194
  */
@@ -199,6 +265,13 @@ export class InputModel implements IInputModel {
199
265
  return this._selectionWatcher;
200
266
  }
201
267
 
268
+ /**
269
+ * Get the document manager.
270
+ */
271
+ get documentManager(): IDocumentManager | null {
272
+ return this._documentManager;
273
+ }
274
+
202
275
  /**
203
276
  * The input configuration.
204
277
  */
@@ -297,6 +370,40 @@ export class InputModel implements IInputModel {
297
370
  this.value = this.value.slice(0, start) + newWord + this.value.slice(end);
298
371
  }
299
372
 
373
+ /**
374
+ * The mentioned user list.
375
+ */
376
+ get mentions(): IUser[] {
377
+ return this._mentions;
378
+ }
379
+
380
+ /**
381
+ * Add a user mention.
382
+ */
383
+ addMention(user: IUser): void {
384
+ const usernames = this._mentions.map(user => user.username);
385
+ if (!usernames.includes(user.username)) {
386
+ this._mentions.push(user);
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Remove a user mention.
392
+ */
393
+ removeMention(user: IUser): void {
394
+ const index = this._mentions.indexOf(user);
395
+ if (index > -1) {
396
+ this._mentions.splice(index, 1);
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Clear mentions list.
402
+ */
403
+ clearMentions = (): void => {
404
+ this._mentions = [];
405
+ };
406
+
300
407
  /**
301
408
  * Dispose the input model.
302
409
  */
@@ -314,12 +421,16 @@ export class InputModel implements IInputModel {
314
421
  return this._isDisposed;
315
422
  }
316
423
 
424
+ private _onSend: (input: string, model?: InputModel) => void;
425
+ private _chatContext: IChatContext;
317
426
  private _value: string;
318
427
  private _cursorIndex: number | null = null;
319
428
  private _currentWord: string | null = null;
320
429
  private _attachments: IAttachment[];
430
+ private _mentions: IUser[];
321
431
  private _activeCellManager: IActiveCellManager | null;
322
432
  private _selectionWatcher: ISelectionWatcher | null;
433
+ private _documentManager: IDocumentManager | null;
323
434
  private _config: InputModel.IConfig;
324
435
  private _valueChanged = new Signal<IInputModel, string>(this);
325
436
  private _cursorIndexChanged = new Signal<IInputModel, number | null>(this);
@@ -332,6 +443,23 @@ export class InputModel implements IInputModel {
332
443
 
333
444
  export namespace InputModel {
334
445
  export interface IOptions {
446
+ /**
447
+ * The chat context (a readonly subset of the chat model).
448
+ */
449
+ chatContext: IChatContext;
450
+
451
+ /**
452
+ * The function that should send the message.
453
+ * @param content - the content of the message.
454
+ * @param model - the model of the input sending the message.
455
+ */
456
+ onSend: (content: string, model?: InputModel) => void;
457
+
458
+ /**
459
+ * Function that should cancel the message edition.
460
+ */
461
+ onCancel?: () => void;
462
+
335
463
  /**
336
464
  * The initial value of the input.
337
465
  */
@@ -342,6 +470,11 @@ export namespace InputModel {
342
470
  */
343
471
  attachments?: IAttachment[];
344
472
 
473
+ /**
474
+ * The initial mentions.
475
+ */
476
+ mentions?: IUser[];
477
+
345
478
  /**
346
479
  * The current cursor index.
347
480
  * This refers to the index of the character in front of the cursor.
@@ -362,6 +495,11 @@ export namespace InputModel {
362
495
  * Selection watcher.
363
496
  */
364
497
  selectionWatcher?: ISelectionWatcher | null;
498
+
499
+ /**
500
+ * Document manager.
501
+ */
502
+ documentManager?: IDocumentManager | null;
365
503
  }
366
504
 
367
505
  export interface IConfig {