@jupyter/chat 0.13.0 → 0.14.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 (90) hide show
  1. package/lib/active-cell-manager.d.ts +2 -0
  2. package/lib/active-cell-manager.js +7 -2
  3. package/lib/components/avatar.d.ts +20 -0
  4. package/lib/components/avatar.js +29 -0
  5. package/lib/components/chat.d.ts +1 -3
  6. package/lib/components/chat.js +2 -3
  7. package/lib/components/index.d.ts +2 -3
  8. package/lib/components/index.js +2 -3
  9. package/lib/components/input/buttons/send-button.js +15 -5
  10. package/lib/components/{chat-input.d.ts → input/chat-input.d.ts} +3 -3
  11. package/lib/components/{chat-input.js → input/chat-input.js} +7 -4
  12. package/lib/components/input/index.d.ts +1 -0
  13. package/lib/components/input/index.js +1 -0
  14. package/lib/components/input/toolbar-registry.d.ts +6 -0
  15. package/lib/components/input/use-chat-commands.d.ts +1 -1
  16. package/lib/components/input/use-chat-commands.js +23 -12
  17. package/lib/components/messages/footer.d.ts +2 -2
  18. package/lib/components/messages/footer.js +1 -1
  19. package/lib/components/messages/header.d.ts +16 -0
  20. package/lib/components/messages/header.js +85 -0
  21. package/lib/components/messages/index.d.ts +9 -0
  22. package/lib/components/messages/index.js +13 -0
  23. package/lib/components/messages/message-renderer.js +1 -1
  24. package/lib/components/messages/message.d.ts +21 -0
  25. package/lib/components/messages/message.js +102 -0
  26. package/lib/components/messages/messages.d.ts +38 -0
  27. package/lib/components/messages/messages.js +139 -0
  28. package/lib/components/messages/navigation.d.ts +20 -0
  29. package/lib/components/messages/navigation.js +98 -0
  30. package/lib/components/messages/writers.d.ts +16 -0
  31. package/lib/components/messages/writers.js +39 -0
  32. package/lib/context.d.ts +1 -1
  33. package/lib/index.d.ts +2 -6
  34. package/lib/index.js +2 -6
  35. package/lib/{registry.d.ts → registers/attachment-openers.d.ts} +1 -1
  36. package/lib/registers/chat-commands.d.ts +108 -0
  37. package/lib/{chat-commands/registry.js → registers/chat-commands.js} +8 -8
  38. package/lib/{footers/registry.d.ts → registers/footers.d.ts} +30 -5
  39. package/lib/registers/index.d.ts +3 -0
  40. package/lib/{footers → registers}/index.js +3 -2
  41. package/lib/selection-watcher.d.ts +11 -1
  42. package/lib/selection-watcher.js +10 -4
  43. package/lib/widgets/index.d.ts +3 -0
  44. package/lib/{chat-commands → widgets}/index.js +3 -2
  45. package/package.json +3 -1
  46. package/src/active-cell-manager.ts +10 -1
  47. package/src/components/avatar.tsx +68 -0
  48. package/src/components/chat.tsx +11 -6
  49. package/src/components/index.ts +2 -3
  50. package/src/components/input/buttons/send-button.tsx +17 -5
  51. package/src/components/{chat-input.tsx → input/chat-input.tsx} +12 -7
  52. package/src/components/input/index.ts +1 -0
  53. package/src/components/input/toolbar-registry.tsx +6 -0
  54. package/src/components/input/use-chat-commands.tsx +30 -15
  55. package/src/components/messages/footer.tsx +5 -2
  56. package/src/components/messages/header.tsx +133 -0
  57. package/src/components/messages/index.ts +14 -0
  58. package/src/components/messages/message-renderer.tsx +1 -1
  59. package/src/components/messages/message.tsx +156 -0
  60. package/src/components/messages/messages.tsx +218 -0
  61. package/src/components/messages/navigation.tsx +167 -0
  62. package/src/components/messages/welcome.tsx +1 -0
  63. package/src/components/messages/writers.tsx +81 -0
  64. package/src/context.ts +1 -1
  65. package/src/index.ts +2 -6
  66. package/src/{registry.ts → registers/attachment-openers.ts} +2 -1
  67. package/src/registers/chat-commands.ts +142 -0
  68. package/src/{footers/registry.ts → registers/footers.ts} +35 -8
  69. package/src/{footers → registers}/index.ts +3 -2
  70. package/src/selection-watcher.ts +28 -5
  71. package/src/{chat-commands → widgets}/index.ts +3 -2
  72. package/style/chat.css +82 -0
  73. package/lib/chat-commands/index.d.ts +0 -2
  74. package/lib/chat-commands/registry.d.ts +0 -28
  75. package/lib/chat-commands/types.d.ts +0 -52
  76. package/lib/chat-commands/types.js +0 -5
  77. package/lib/components/chat-messages.d.ts +0 -119
  78. package/lib/components/chat-messages.js +0 -446
  79. package/lib/footers/index.d.ts +0 -2
  80. package/lib/footers/types.d.ts +0 -26
  81. package/lib/footers/types.js +0 -5
  82. package/src/chat-commands/registry.ts +0 -60
  83. package/src/chat-commands/types.ts +0 -67
  84. package/src/components/chat-messages.tsx +0 -739
  85. package/src/footers/types.ts +0 -33
  86. package/lib/components/{toolbar.d.ts → messages/toolbar.d.ts} +0 -0
  87. package/lib/components/{toolbar.js → messages/toolbar.js} +0 -0
  88. package/lib/{registry.js → registers/attachment-openers.js} +0 -0
  89. package/lib/{footers/registry.js → registers/footers.js} +4 -4
  90. /package/src/components/{toolbar.tsx → messages/toolbar.tsx} +0 -0
@@ -1,5 +1,11 @@
1
+ /// <reference types="react" />
1
2
  import { Token } from '@lumino/coreutils';
2
- import { MessageFooter, MessageFooterSection } from './types';
3
+ import { IChatModel } from '../model';
4
+ import { IChatMessage } from '../types';
5
+ /**
6
+ * The token providing the chat footer registry.
7
+ */
8
+ export declare const IMessageFooterRegistry: Token<IMessageFooterRegistry>;
3
9
  /**
4
10
  * The interface of a registry to provide chat footer.
5
11
  */
@@ -15,6 +21,29 @@ export interface IMessageFooterRegistry {
15
21
  */
16
22
  addSection(section: MessageFooterSection): void;
17
23
  }
24
+ /**
25
+ * The props sent passed to each `MessageFooterSection` React component.
26
+ */
27
+ export type MessageFooterSectionProps = {
28
+ model: IChatModel;
29
+ message: IChatMessage;
30
+ };
31
+ /**
32
+ * A message footer section which can be added to the footer registry.
33
+ */
34
+ export type MessageFooterSection = {
35
+ component: React.FC<MessageFooterSectionProps>;
36
+ position: 'left' | 'center' | 'right';
37
+ };
38
+ /**
39
+ * The message footer returned by the registry, composed of 'left', 'center',
40
+ * and 'right' sections.
41
+ */
42
+ export type MessageFooter = {
43
+ left?: MessageFooterSection;
44
+ center?: MessageFooterSection;
45
+ right?: MessageFooterSection;
46
+ };
18
47
  /**
19
48
  * The default implementation of the message footer registry.
20
49
  */
@@ -30,7 +59,3 @@ export declare class MessageFooterRegistry implements IMessageFooterRegistry {
30
59
  addSection(footer: MessageFooterSection): void;
31
60
  private _footers;
32
61
  }
33
- /**
34
- * The token providing the chat footer registry.
35
- */
36
- export declare const IMessageFooterRegistry: Token<IMessageFooterRegistry>;
@@ -0,0 +1,3 @@
1
+ export * from './attachment-openers';
2
+ export * from './chat-commands';
3
+ export * from './footers';
@@ -2,5 +2,6 @@
2
2
  * Copyright (c) Jupyter Development Team.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
- export * from './registry';
6
- export * from './types';
5
+ export * from './attachment-openers';
6
+ export * from './chat-commands';
7
+ export * from './footers';
@@ -1,5 +1,6 @@
1
1
  import { JupyterFrontEnd } from '@jupyterlab/application';
2
2
  import { CodeEditor } from '@jupyterlab/codeeditor';
3
+ import { IEditorLanguageRegistry } from '@jupyterlab/codemirror';
3
4
  import { Widget } from '@lumino/widgets';
4
5
  import { ISignal, Signal } from '@lumino/signaling';
5
6
  /**
@@ -14,6 +15,10 @@ export declare namespace SelectionWatcher {
14
15
  * The current shell of the application.
15
16
  */
16
17
  shell: JupyterFrontEnd.IShell;
18
+ /**
19
+ * Editor language registry.
20
+ */
21
+ languages?: IEditorLanguageRegistry;
17
22
  }
18
23
  /**
19
24
  * The selection type.
@@ -31,6 +36,10 @@ export declare namespace SelectionWatcher {
31
36
  * The ID of the document widget in which the selection was made.
32
37
  */
33
38
  widgetId: string;
39
+ /**
40
+ * The language of the selection.
41
+ */
42
+ language?: string;
34
43
  /**
35
44
  * The ID of the cell in which the selection was made, if the original widget
36
45
  * was a notebook.
@@ -54,9 +63,10 @@ export declare class SelectionWatcher {
54
63
  get selection(): SelectionWatcher.Selection | null;
55
64
  get selectionChanged(): ISignal<this, SelectionWatcher.Selection | null>;
56
65
  replaceSelection(selection: SelectionWatcher.Selection): void;
57
- protected _poll(): void;
66
+ protected _poll(): Promise<void>;
58
67
  protected _shell: JupyterFrontEnd.IShell;
59
68
  protected _mainAreaDocumentWidget: Widget | null;
60
69
  protected _selection: SelectionWatcher.Selection | null;
61
70
  protected _selectionChanged: Signal<this, SelectionWatcher.Selection | null>;
71
+ private _languages;
62
72
  }
@@ -3,6 +3,7 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
  import { DocumentWidget } from '@jupyterlab/docregistry';
6
+ import { EditorLanguageRegistry } from '@jupyterlab/codemirror';
6
7
  import { Notebook } from '@jupyterlab/notebook';
7
8
  import { find } from '@lumino/algorithm';
8
9
  import { Signal } from '@lumino/signaling';
@@ -17,6 +18,7 @@ export class SelectionWatcher {
17
18
  this._selection = null;
18
19
  this._selectionChanged = new Signal(this);
19
20
  this._shell = options.shell;
21
+ this._languages = options.languages || new EditorLanguageRegistry();
20
22
  (_a = this._shell.currentChanged) === null || _a === void 0 ? void 0 : _a.connect((sender, args) => {
21
23
  var _a;
22
24
  // Do not change the main area widget if the new one has no editor, for example
@@ -76,13 +78,13 @@ export class SelectionWatcher {
76
78
  const newPosition = editor.getPositionAt(editor.getOffsetAt(selection.start) + selection.text.length);
77
79
  editor.setSelection({ start: newPosition, end: newPosition });
78
80
  }
79
- _poll() {
81
+ async _poll() {
80
82
  var _a;
81
83
  let currSelection = null;
82
84
  const prevSelection = this._selection;
83
85
  // Do not return selected text if the main area widget is hidden.
84
86
  if ((_a = this._mainAreaDocumentWidget) === null || _a === void 0 ? void 0 : _a.isVisible) {
85
- currSelection = getTextSelection(this._mainAreaDocumentWidget);
87
+ currSelection = await getTextSelection(this._mainAreaDocumentWidget, this._languages);
86
88
  }
87
89
  if ((prevSelection === null || prevSelection === void 0 ? void 0 : prevSelection.text) !== (currSelection === null || currSelection === void 0 ? void 0 : currSelection.text)) {
88
90
  this._selection = currSelection;
@@ -93,8 +95,8 @@ export class SelectionWatcher {
93
95
  /**
94
96
  * Gets a Selection object from a document widget. Returns `null` if unable.
95
97
  */
96
- function getTextSelection(widget) {
97
- var _a;
98
+ async function getTextSelection(widget, languages) {
99
+ var _a, _b;
98
100
  const editor = getEditor(widget);
99
101
  // widget type check is redundant but hints the type to TypeScript
100
102
  if (!editor || !(widget instanceof DocumentWidget)) {
@@ -120,6 +122,7 @@ function getTextSelection(widget) {
120
122
  if (startOffset > endOffset) {
121
123
  [start, end] = [end, start];
122
124
  }
125
+ const language = (_b = (await languages.getLanguage(editor === null || editor === void 0 ? void 0 : editor.model.mimeType))) === null || _b === void 0 ? void 0 : _b.name;
123
126
  return {
124
127
  ...selectionObj,
125
128
  start,
@@ -127,6 +130,9 @@ function getTextSelection(widget) {
127
130
  text,
128
131
  numLines: text.split('\n').length,
129
132
  widgetId: widget.id,
133
+ ...(language && {
134
+ language
135
+ }),
130
136
  ...(cellId && {
131
137
  cellId
132
138
  })
@@ -0,0 +1,3 @@
1
+ export * from './chat-error';
2
+ export * from './chat-sidebar';
3
+ export * from './chat-widget';
@@ -2,5 +2,6 @@
2
2
  * Copyright (c) Jupyter Development Team.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
- export * from './types';
6
- export * from './registry';
5
+ export * from './chat-error';
6
+ export * from './chat-sidebar';
7
+ export * from './chat-widget';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyter/chat",
3
- "version": "0.13.0",
3
+ "version": "0.14.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",
@@ -48,6 +48,8 @@
48
48
  "@jupyter/react-components": "^0.15.2",
49
49
  "@jupyterlab/application": "^4.2.0",
50
50
  "@jupyterlab/apputils": "^4.3.0",
51
+ "@jupyterlab/codeeditor": "^4.2.0",
52
+ "@jupyterlab/codemirror": "^4.2.0",
51
53
  "@jupyterlab/docmanager": "^4.2.0",
52
54
  "@jupyterlab/filebrowser": "^4.2.0",
53
55
  "@jupyterlab/fileeditor": "^4.2.0",
@@ -13,11 +13,13 @@ import { ISignal, Signal } from '@lumino/signaling';
13
13
  type CellContent = {
14
14
  type: string;
15
15
  source: string;
16
+ language?: string;
16
17
  };
17
18
 
18
19
  type CellWithErrorContent = {
19
20
  type: 'code';
20
21
  source: string;
22
+ language?: string;
21
23
  error: {
22
24
  name: string;
23
25
  value: string;
@@ -148,6 +150,11 @@ export class ActiveCellManager implements IActiveCellManager {
148
150
  getContent(withError: true): CellWithErrorContent | null;
149
151
  getContent(withError = false): CellContent | CellWithErrorContent | null {
150
152
  const sharedModel = this._notebookTracker.activeCell?.model.sharedModel;
153
+ const language =
154
+ sharedModel?.cell_type === 'code'
155
+ ? this._notebookTracker.currentWidget?.model?.defaultKernelLanguage
156
+ : undefined;
157
+
151
158
  if (!sharedModel) {
152
159
  return null;
153
160
  }
@@ -156,7 +163,8 @@ export class ActiveCellManager implements IActiveCellManager {
156
163
  if (!withError) {
157
164
  return {
158
165
  type: sharedModel.cell_type,
159
- source: sharedModel.getSource()
166
+ source: sharedModel.getSource(),
167
+ language
160
168
  };
161
169
  }
162
170
 
@@ -166,6 +174,7 @@ export class ActiveCellManager implements IActiveCellManager {
166
174
  return {
167
175
  type: 'code',
168
176
  source: sharedModel.getSource(),
177
+ language,
169
178
  error: {
170
179
  name: error.ename,
171
180
  value: error.evalue,
@@ -0,0 +1,68 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import { Avatar as MuiAvatar, Typography } from '@mui/material';
7
+ import type { SxProps, Theme } from '@mui/material';
8
+ import React from 'react';
9
+
10
+ import { IUser } from '../types';
11
+
12
+ /**
13
+ * The avatar props.
14
+ */
15
+ type AvatarProps = {
16
+ /**
17
+ * The user to display an avatar.
18
+ */
19
+ user: IUser;
20
+ /**
21
+ * Whether the avatar should be small.
22
+ */
23
+ small?: boolean;
24
+ };
25
+
26
+ /**
27
+ * The avatar component.
28
+ */
29
+ export function Avatar(props: AvatarProps): JSX.Element | null {
30
+ const { user } = props;
31
+
32
+ const sharedStyles: SxProps<Theme> = {
33
+ height: `${props.small ? '16' : '24'}px`,
34
+ width: `${props.small ? '16' : '24'}px`,
35
+ bgcolor: user.color,
36
+ fontSize: `var(--jp-ui-font-size${props.small ? '0' : '1'})`
37
+ };
38
+
39
+ const name =
40
+ user.display_name ?? user.name ?? (user.username || 'User undefined');
41
+ return user.avatar_url ? (
42
+ <MuiAvatar
43
+ sx={{
44
+ ...sharedStyles
45
+ }}
46
+ src={user.avatar_url}
47
+ alt={name}
48
+ title={name}
49
+ ></MuiAvatar>
50
+ ) : user.initials ? (
51
+ <MuiAvatar
52
+ sx={{
53
+ ...sharedStyles
54
+ }}
55
+ alt={name}
56
+ title={name}
57
+ >
58
+ <Typography
59
+ sx={{
60
+ fontSize: `var(--jp-ui-font-size${props.small ? '0' : '1'})`,
61
+ color: 'var(--jp-ui-inverse-font-color1)'
62
+ }}
63
+ >
64
+ {user.initials}
65
+ </Typography>
66
+ </MuiAvatar>
67
+ ) : null;
68
+ }
@@ -11,15 +11,20 @@ import { IconButton } from '@mui/material';
11
11
  import { Box } from '@mui/system';
12
12
  import React, { useState } from 'react';
13
13
 
14
+ import {
15
+ ChatInput,
16
+ IInputToolbarRegistry,
17
+ InputToolbarRegistry
18
+ } from './input';
14
19
  import { JlThemeProvider } from './jl-theme-provider';
15
- import { IChatCommandRegistry } from '../chat-commands';
16
- import { ChatMessages } from './chat-messages';
17
- import { ChatInput } from './chat-input';
18
- import { IInputToolbarRegistry, InputToolbarRegistry } from './input';
20
+ import { ChatMessages } from './messages';
19
21
  import { AttachmentOpenerContext } from '../context';
20
- import { IMessageFooterRegistry } from '../footers';
21
22
  import { IChatModel } from '../model';
22
- import { IAttachmentOpenerRegistry } from '../registry';
23
+ import {
24
+ IAttachmentOpenerRegistry,
25
+ IChatCommandRegistry,
26
+ IMessageFooterRegistry
27
+ } from '../registers';
23
28
 
24
29
  export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
25
30
  const { model } = props;
@@ -3,12 +3,11 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
 
6
+ export * from './avatar';
6
7
  export * from './chat';
7
- export * from './chat-input';
8
- export * from './chat-messages';
9
8
  export * from './code-blocks';
10
9
  export * from './input';
11
10
  export * from './jl-theme-provider';
11
+ export * from './messages';
12
12
  export * from './mui-extras';
13
13
  export * from './scroll-container';
14
- export * from './toolbar';
@@ -23,7 +23,7 @@ const SEND_INCLUDE_LI_CLASS = 'jp-chat-send-include';
23
23
  export function SendButton(
24
24
  props: InputToolbarRegistry.IToolbarItemProps
25
25
  ): JSX.Element {
26
- const { model } = props;
26
+ const { model, chatCommandRegistry } = props;
27
27
  const { activeCellManager, selectionWatcher } = model;
28
28
  const hideIncludeSelection = !activeCellManager || !selectionWatcher;
29
29
 
@@ -98,21 +98,33 @@ export function SendButton(
98
98
  };
99
99
  }, [activeCellManager, selectionWatcher]);
100
100
 
101
- function sendWithSelection() {
101
+ async function send() {
102
+ await chatCommandRegistry?.onSubmit(model);
103
+ model.send(model.value);
104
+ }
105
+
106
+ async function sendWithSelection() {
102
107
  let source = '';
103
108
 
109
+ // Run all chat command providers
110
+ await chatCommandRegistry?.onSubmit(model);
111
+
112
+ let language: string | undefined;
104
113
  if (selectionWatcher?.selection) {
105
114
  // Append the selected text if exists.
106
115
  source = selectionWatcher.selection.text;
116
+ language = selectionWatcher.selection.language;
107
117
  } else if (activeCellManager?.available) {
108
118
  // Append the active cell content if exists.
109
- source = activeCellManager.getContent(false)!.source;
119
+ const content = activeCellManager.getContent(false);
120
+ source = content!.source;
121
+ language = content?.language;
110
122
  }
111
123
  let content = model.value;
112
124
  if (source) {
113
125
  content += `
114
126
 
115
- \`\`\`
127
+ \`\`\`${language ?? ''}
116
128
  ${source}
117
129
  \`\`\`
118
130
  `;
@@ -125,7 +137,7 @@ ${source}
125
137
  return (
126
138
  <>
127
139
  <TooltippedButton
128
- onClick={() => model.send(model.value)}
140
+ onClick={send}
129
141
  disabled={disabled}
130
142
  tooltip={tooltip}
131
143
  buttonProps={{
@@ -15,15 +15,15 @@ import {
15
15
  import clsx from 'clsx';
16
16
  import React, { useEffect, useRef, useState } from 'react';
17
17
 
18
- import { AttachmentPreviewList } from './attachments';
18
+ import { AttachmentPreviewList } from '../attachments';
19
19
  import {
20
20
  IInputToolbarRegistry,
21
21
  InputToolbarRegistry,
22
22
  useChatCommands
23
- } from './input';
24
- import { IInputModel, InputModel } from '../input-model';
25
- import { IChatCommandRegistry } from '../chat-commands';
26
- import { IAttachment } from '../types';
23
+ } from '.';
24
+ import { IInputModel, InputModel } from '../../input-model';
25
+ import { IChatCommandRegistry } from '../../registers';
26
+ import { IAttachment } from '../../types';
27
27
 
28
28
  const INPUT_BOX_CLASS = 'jp-chat-input-container';
29
29
  const INPUT_TOOLBAR_CLASS = 'jp-chat-input-toolbar';
@@ -105,7 +105,7 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
105
105
  * "Enter". This also handles many of the edge cases in the MUI Autocomplete
106
106
  * component.
107
107
  */
108
- function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
108
+ async function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
109
109
  /**
110
110
  * IMPORTANT: This statement ensures that arrow keys can be used to navigate
111
111
  * the multiline input when the chat commands menu is closed.
@@ -157,6 +157,8 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
157
157
  (sendWithShiftEnter && event.shiftKey) ||
158
158
  (!sendWithShiftEnter && !event.shiftKey)
159
159
  ) {
160
+ // Run all command providers
161
+ await props.chatCommandRegistry?.onSubmit(model);
160
162
  model.send(input);
161
163
  event.stopPropagation();
162
164
  event.preventDefault();
@@ -218,7 +220,10 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
218
220
  endAdornment: (
219
221
  <InputAdornment position="end" className={INPUT_TOOLBAR_CLASS}>
220
222
  {toolbarElements.map(item => (
221
- <item.element model={model} />
223
+ <item.element
224
+ model={model}
225
+ chatCommandRegistry={props.chatCommandRegistry}
226
+ />
222
227
  ))}
223
228
  </InputAdornment>
224
229
  )
@@ -4,5 +4,6 @@
4
4
  */
5
5
 
6
6
  export * from './buttons';
7
+ export * from './chat-input';
7
8
  export * from './toolbar-registry';
8
9
  export * from './use-chat-commands';
@@ -7,6 +7,7 @@ import * as React from 'react';
7
7
  import { AttachButton, CancelButton, SendButton } from './buttons';
8
8
  import { IInputModel } from '../../input-model';
9
9
  import { ISignal, Signal } from '@lumino/signaling';
10
+ import { IChatCommandRegistry } from '../../registers';
10
11
 
11
12
  /**
12
13
  * The toolbar registry interface.
@@ -137,6 +138,11 @@ export namespace InputToolbarRegistry {
137
138
  * The input model of the input component including the button.
138
139
  */
139
140
  model: IInputModel;
141
+ /**
142
+ * Chat command registry. Should be used by the "Send" button to run
143
+ * `onSubmit()` on all command providers before sending the message.
144
+ */
145
+ chatCommandRegistry?: IChatCommandRegistry;
140
146
  }
141
147
 
142
148
  /**
@@ -11,7 +11,7 @@ import type {
11
11
  import { Box } from '@mui/material';
12
12
  import React, { useEffect, useState } from 'react';
13
13
 
14
- import { ChatCommand, IChatCommandRegistry } from '../../chat-commands';
14
+ import { ChatCommand, IChatCommandRegistry } from '../../registers';
15
15
  import { IInputModel } from '../../input-model';
16
16
 
17
17
  type AutocompleteProps = GenericAutocompleteProps<any, any, any, any>;
@@ -45,6 +45,9 @@ export function useChatCommands(
45
45
  const [commands, setCommands] = useState<ChatCommand[]>([]);
46
46
 
47
47
  useEffect(() => {
48
+ /**
49
+ * Callback that runs whenever the current word changes.
50
+ */
48
51
  async function getCommands(_: IInputModel, currentWord: string | null) {
49
52
  const providers = chatCommandRegistry?.getProviders();
50
53
  if (!providers) {
@@ -58,12 +61,12 @@ export function useChatCommands(
58
61
  return;
59
62
  }
60
63
 
61
- let newCommands: ChatCommand[] = [];
64
+ let commandCompletions: ChatCommand[] = [];
62
65
  for (const provider of providers) {
63
66
  // TODO: optimize performance when this method is truly async
64
67
  try {
65
- newCommands = newCommands.concat(
66
- await provider.getChatCommands(inputModel)
68
+ commandCompletions = commandCompletions.concat(
69
+ await provider.listCommandCompletions(inputModel)
67
70
  );
68
71
  } catch (e) {
69
72
  console.error(
@@ -72,10 +75,23 @@ export function useChatCommands(
72
75
  );
73
76
  }
74
77
  }
75
- if (newCommands) {
76
- setOpen(true);
78
+
79
+ // Immediately replace the current word if it exactly matches one command
80
+ // and 'replaceWith' is set.
81
+ if (
82
+ commandCompletions.length === 1 &&
83
+ commandCompletions[0].name === inputModel.currentWord &&
84
+ commandCompletions[0].replaceWith !== undefined
85
+ ) {
86
+ const replacement = commandCompletions[0].replaceWith;
87
+ inputModel.replaceCurrentWord(replacement);
88
+ return;
77
89
  }
78
- setCommands(newCommands);
90
+
91
+ // Otherwise, open/close the menu based on the presence of command
92
+ // completions and set the menu entries.
93
+ setOpen(!!commandCompletions.length);
94
+ setCommands(commandCompletions);
79
95
  }
80
96
 
81
97
  inputModel.currentWordChanged.connect(getCommands);
@@ -87,7 +103,8 @@ export function useChatCommands(
87
103
 
88
104
  /**
89
105
  * onChange(): the callback invoked when a command is selected from the chat
90
- * commands menu by the user.
106
+ * commands menu. When a command `cmd` is selected, this function replaces the
107
+ * current word with `cmd.replaceWith` if set, `cmd.name` otherwise.
91
108
  */
92
109
  const onChange: AutocompleteProps['onChange'] = (
93
110
  e: unknown,
@@ -109,14 +126,12 @@ export function useChatCommands(
109
126
  return;
110
127
  }
111
128
 
112
- // if replaceWith is set, handle the command immediately
113
- if (command.replaceWith) {
114
- inputModel.replaceCurrentWord(command.replaceWith);
115
- return;
129
+ let replacement =
130
+ command.replaceWith === undefined ? command.name : command.replaceWith;
131
+ if (command.spaceOnAccept) {
132
+ replacement += ' ';
116
133
  }
117
-
118
- // otherwise, defer handling to the command provider
119
- chatCommandRegistry.handleChatCommand(command, inputModel);
134
+ inputModel.replaceCurrentWord(replacement);
120
135
  };
121
136
 
122
137
  return {
@@ -5,10 +5,11 @@
5
5
 
6
6
  import { Box } from '@mui/material';
7
7
  import React from 'react';
8
+
8
9
  import {
9
10
  IMessageFooterRegistry,
10
11
  MessageFooterSectionProps
11
- } from '../../footers';
12
+ } from '../../registers';
12
13
 
13
14
  /**
14
15
  * The chat footer component properties.
@@ -24,7 +25,9 @@ export interface IMessageFootersProps extends MessageFooterSectionProps {
24
25
  * The chat footer component, which displays footer components on a row according to
25
26
  * their respective positions.
26
27
  */
27
- export function MessageFooter(props: IMessageFootersProps): JSX.Element {
28
+ export function MessageFooterComponent(
29
+ props: IMessageFootersProps
30
+ ): JSX.Element {
28
31
  const { message, model, registry } = props;
29
32
  const footer = registry.getFooter();
30
33