@jupyter/chat 0.7.0 → 0.8.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.
- package/lib/active-cell-manager.js +1 -4
- package/lib/chat-commands/index.d.ts +2 -0
- package/lib/chat-commands/index.js +6 -0
- package/lib/chat-commands/registry.d.ts +28 -0
- package/lib/chat-commands/registry.js +29 -0
- package/lib/chat-commands/types.d.ts +51 -0
- package/lib/chat-commands/types.js +5 -0
- package/lib/components/attachments.d.ts +23 -0
- package/lib/components/attachments.js +44 -0
- package/lib/components/chat-input.d.ts +8 -11
- package/lib/components/chat-input.js +70 -95
- package/lib/components/chat-messages.d.ts +4 -0
- package/lib/components/chat-messages.js +27 -1
- package/lib/components/chat.d.ts +11 -5
- package/lib/components/chat.js +7 -8
- package/lib/components/input/attach-button.d.ts +14 -0
- package/lib/components/input/attach-button.js +45 -0
- package/lib/components/input/index.d.ts +1 -0
- package/lib/components/input/index.js +1 -0
- package/lib/components/input/send-button.d.ts +2 -2
- package/lib/components/input/use-chat-commands.d.ts +19 -0
- package/lib/components/input/use-chat-commands.js +127 -0
- package/lib/context.d.ts +3 -0
- package/lib/context.js +6 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/input-model.d.ts +221 -0
- package/lib/input-model.js +217 -0
- package/lib/model.d.ts +10 -25
- package/lib/model.js +15 -17
- package/lib/registry.d.ts +11 -64
- package/lib/registry.js +4 -72
- package/lib/types.d.ts +19 -38
- package/lib/widgets/chat-widget.js +2 -1
- package/package.json +3 -114
- package/src/active-cell-manager.ts +0 -3
- package/src/chat-commands/index.ts +7 -0
- package/src/chat-commands/registry.ts +60 -0
- package/src/chat-commands/types.ts +67 -0
- package/src/components/attachments.tsx +91 -0
- package/src/components/chat-input.tsx +97 -124
- package/src/components/chat-messages.tsx +36 -3
- package/src/components/chat.tsx +28 -19
- package/src/components/input/attach-button.tsx +68 -0
- package/src/components/input/cancel-button.tsx +1 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input/send-button.tsx +2 -2
- package/src/components/input/use-chat-commands.tsx +186 -0
- package/src/context.ts +10 -0
- package/src/index.ts +2 -0
- package/src/input-model.ts +406 -0
- package/src/model.ts +24 -35
- package/src/registry.ts +14 -108
- package/src/types.ts +19 -39
- package/src/widgets/chat-widget.tsx +2 -1
- package/style/chat.css +27 -9
- package/style/icons/include-selection.svg +3 -1
- package/style/icons/read.svg +8 -6
- package/style/icons/replace-cell.svg +10 -6
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
import type {
|
|
9
|
+
AutocompleteChangeReason,
|
|
10
|
+
AutocompleteProps as GenericAutocompleteProps
|
|
11
|
+
} from '@mui/material';
|
|
12
|
+
import { Box } from '@mui/material';
|
|
13
|
+
|
|
14
|
+
import { ChatCommand, IChatCommandRegistry } from '../../chat-commands';
|
|
15
|
+
import { IInputModel } from '../../input-model';
|
|
16
|
+
|
|
17
|
+
type AutocompleteProps = GenericAutocompleteProps<any, any, any, any>;
|
|
18
|
+
|
|
19
|
+
type UseChatCommandsReturn = {
|
|
20
|
+
autocompleteProps: Omit<AutocompleteProps, 'renderInput'>;
|
|
21
|
+
menu: {
|
|
22
|
+
open: boolean;
|
|
23
|
+
highlighted: boolean;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A hook which automatically returns the list of command options given the
|
|
29
|
+
* current input and chat command registry.
|
|
30
|
+
*
|
|
31
|
+
* Intended usage: `const chatCommands = useChatCommands(...)`.
|
|
32
|
+
*/
|
|
33
|
+
export function useChatCommands(
|
|
34
|
+
inputModel: IInputModel,
|
|
35
|
+
chatCommandRegistry?: IChatCommandRegistry
|
|
36
|
+
): UseChatCommandsReturn {
|
|
37
|
+
// whether an option is highlighted in the chat commands menu
|
|
38
|
+
const [highlighted, setHighlighted] = useState(false);
|
|
39
|
+
|
|
40
|
+
// whether the chat commands menu is open
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
42
|
+
|
|
43
|
+
// current list of chat commands matched by the current word.
|
|
44
|
+
// the current word is the space-separated word at the user's cursor.
|
|
45
|
+
const [commands, setCommands] = useState<ChatCommand[]>([]);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
async function getCommands(_: IInputModel, currentWord: string | null) {
|
|
49
|
+
const providers = chatCommandRegistry?.getProviders();
|
|
50
|
+
if (!providers) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!currentWord?.length) {
|
|
55
|
+
setCommands([]);
|
|
56
|
+
setOpen(false);
|
|
57
|
+
setHighlighted(false);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let newCommands: ChatCommand[] = [];
|
|
62
|
+
for (const provider of providers) {
|
|
63
|
+
// TODO: optimize performance when this method is truly async
|
|
64
|
+
try {
|
|
65
|
+
newCommands = newCommands.concat(
|
|
66
|
+
await provider.getChatCommands(inputModel)
|
|
67
|
+
);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.error(
|
|
70
|
+
`Error when getting chat commands from command provider '${provider.id}': `,
|
|
71
|
+
e
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (newCommands) {
|
|
76
|
+
setOpen(true);
|
|
77
|
+
}
|
|
78
|
+
setCommands(newCommands);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
inputModel.currentWordChanged.connect(getCommands);
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
inputModel.currentWordChanged.disconnect(getCommands);
|
|
85
|
+
};
|
|
86
|
+
}, [inputModel]);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* onChange(): the callback invoked when a command is selected from the chat
|
|
90
|
+
* commands menu by the user.
|
|
91
|
+
*/
|
|
92
|
+
const onChange: AutocompleteProps['onChange'] = (
|
|
93
|
+
e: unknown,
|
|
94
|
+
command: ChatCommand,
|
|
95
|
+
reason: AutocompleteChangeReason
|
|
96
|
+
) => {
|
|
97
|
+
if (reason !== 'selectOption') {
|
|
98
|
+
// only call this callback when a command is selected by the user. this
|
|
99
|
+
// requires `reason === 'selectOption'`.
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!chatCommandRegistry) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const currentWord = inputModel.currentWord;
|
|
108
|
+
if (!currentWord) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// if replaceWith is set, handle the command immediately
|
|
113
|
+
if (command.replaceWith) {
|
|
114
|
+
inputModel.replaceCurrentWord(command.replaceWith);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// otherwise, defer handling to the command provider
|
|
119
|
+
chatCommandRegistry.handleChatCommand(command, inputModel);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
autocompleteProps: {
|
|
124
|
+
open,
|
|
125
|
+
options: commands,
|
|
126
|
+
getOptionLabel: (command: ChatCommand) => command.name,
|
|
127
|
+
renderOption: (
|
|
128
|
+
defaultProps,
|
|
129
|
+
command: ChatCommand,
|
|
130
|
+
__: unknown,
|
|
131
|
+
___: unknown
|
|
132
|
+
) => {
|
|
133
|
+
const { key, ...listItemProps } = defaultProps;
|
|
134
|
+
const commandIcon: JSX.Element = (
|
|
135
|
+
<span>
|
|
136
|
+
{typeof command.icon === 'object' ? (
|
|
137
|
+
<command.icon.react />
|
|
138
|
+
) : (
|
|
139
|
+
command.icon
|
|
140
|
+
)}
|
|
141
|
+
</span>
|
|
142
|
+
);
|
|
143
|
+
return (
|
|
144
|
+
<Box key={key} component="li" {...listItemProps}>
|
|
145
|
+
{commandIcon}
|
|
146
|
+
<p className="jp-chat-command-name">{command.name}</p>
|
|
147
|
+
<span> - </span>
|
|
148
|
+
<p className="jp-chat-command-description">{command.description}</p>
|
|
149
|
+
</Box>
|
|
150
|
+
);
|
|
151
|
+
},
|
|
152
|
+
// always show all options, since command providers should exclusively
|
|
153
|
+
// define what commands are added to the menu.
|
|
154
|
+
filterOptions: (commands: ChatCommand[]) => commands,
|
|
155
|
+
value: null,
|
|
156
|
+
autoHighlight: true,
|
|
157
|
+
freeSolo: true,
|
|
158
|
+
disableClearable: true,
|
|
159
|
+
onChange,
|
|
160
|
+
onHighlightChange:
|
|
161
|
+
/**
|
|
162
|
+
* On highlight change: set `highlighted` to whether an option is
|
|
163
|
+
* highlighted by the user.
|
|
164
|
+
*
|
|
165
|
+
* This isn't called when an option is selected for some reason, so we
|
|
166
|
+
* need to call `setHighlighted(false)` in `onClose()`.
|
|
167
|
+
*/
|
|
168
|
+
(_, highlightedOption) => {
|
|
169
|
+
setHighlighted(!!highlightedOption);
|
|
170
|
+
},
|
|
171
|
+
onClose:
|
|
172
|
+
/**
|
|
173
|
+
* On close: set `highlighted` to `false` and close the popup by
|
|
174
|
+
* setting `open` to `false`.
|
|
175
|
+
*/
|
|
176
|
+
() => {
|
|
177
|
+
setHighlighted(false);
|
|
178
|
+
setOpen(false);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
menu: {
|
|
182
|
+
open,
|
|
183
|
+
highlighted
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { createContext } from 'react';
|
|
6
|
+
import { IAttachmentOpenerRegistry } from './registry';
|
|
7
|
+
|
|
8
|
+
export const AttachmentOpenerContext = createContext<
|
|
9
|
+
IAttachmentOpenerRegistry | undefined
|
|
10
|
+
>(undefined);
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
export * from './active-cell-manager';
|
|
7
7
|
export * from './components';
|
|
8
8
|
export * from './icons';
|
|
9
|
+
export * from './input-model';
|
|
9
10
|
export * from './model';
|
|
10
11
|
export * from './registry';
|
|
11
12
|
export * from './selection-watcher';
|
|
@@ -13,3 +14,4 @@ export * from './types';
|
|
|
13
14
|
export * from './widgets/chat-error';
|
|
14
15
|
export * from './widgets/chat-sidebar';
|
|
15
16
|
export * from './widgets/chat-widget';
|
|
17
|
+
export * from './chat-commands';
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IDisposable } from '@lumino/disposable';
|
|
7
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
8
|
+
import { IActiveCellManager } from './active-cell-manager';
|
|
9
|
+
import { ISelectionWatcher } from './selection-watcher';
|
|
10
|
+
import { IAttachment } from './types';
|
|
11
|
+
|
|
12
|
+
const WHITESPACE = new Set([' ', '\n', '\t']);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The chat input interface.
|
|
16
|
+
*/
|
|
17
|
+
export interface IInputModel extends IDisposable {
|
|
18
|
+
/**
|
|
19
|
+
* The entire input value.
|
|
20
|
+
*/
|
|
21
|
+
value: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A signal emitting when the value has changed.
|
|
25
|
+
*/
|
|
26
|
+
readonly valueChanged: ISignal<IInputModel, string>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The current cursor index.
|
|
30
|
+
* This refers to the index of the character in front of the cursor.
|
|
31
|
+
*/
|
|
32
|
+
cursorIndex: number | null;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A signal emitting when the cursor position has changed.
|
|
36
|
+
*/
|
|
37
|
+
readonly cursorIndexChanged: ISignal<IInputModel, number | null>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The current word behind the user's cursor, space-separated.
|
|
41
|
+
*/
|
|
42
|
+
readonly currentWord: string | null;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A signal emitting when the current word has changed.
|
|
46
|
+
*/
|
|
47
|
+
readonly currentWordChanged: ISignal<IInputModel, string | null>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the active cell manager.
|
|
51
|
+
*/
|
|
52
|
+
readonly activeCellManager: IActiveCellManager | null;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the selection watcher.
|
|
56
|
+
*/
|
|
57
|
+
readonly selectionWatcher: ISelectionWatcher | null;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The input configuration.
|
|
61
|
+
*/
|
|
62
|
+
config: InputModel.IConfig;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A signal emitting when the messages list is updated.
|
|
66
|
+
*/
|
|
67
|
+
readonly configChanged: ISignal<IInputModel, InputModel.IConfig>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Function to request the focus on the input of the chat.
|
|
71
|
+
*/
|
|
72
|
+
focus(): void;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A signal emitting when the focus is requested on the input.
|
|
76
|
+
*/
|
|
77
|
+
readonly focusInputSignal?: ISignal<IInputModel, void>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The attachments list.
|
|
81
|
+
*/
|
|
82
|
+
readonly attachments: IAttachment[];
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Add attachment to the next message to send.
|
|
86
|
+
*/
|
|
87
|
+
addAttachment?(attachment: IAttachment): void;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Remove attachment to the next message to send.
|
|
91
|
+
*/
|
|
92
|
+
removeAttachment?(attachment: IAttachment): void;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Clear the attachment list.
|
|
96
|
+
*/
|
|
97
|
+
clearAttachments(): void;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A signal emitting when the attachment list has changed.
|
|
101
|
+
*/
|
|
102
|
+
readonly attachmentsChanged?: ISignal<IInputModel, IAttachment[]>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Replace the current word in the input with a new one.
|
|
106
|
+
*/
|
|
107
|
+
replaceCurrentWord(newWord: string): void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The input model.
|
|
112
|
+
*/
|
|
113
|
+
export class InputModel implements IInputModel {
|
|
114
|
+
constructor(options: InputModel.IOptions) {
|
|
115
|
+
this._value = options.value || '';
|
|
116
|
+
this._attachments = options.attachments || [];
|
|
117
|
+
this.cursorIndex = options.cursorIndex || this.value.length;
|
|
118
|
+
this._activeCellManager = options.activeCellManager ?? null;
|
|
119
|
+
this._selectionWatcher = options.selectionWatcher ?? null;
|
|
120
|
+
|
|
121
|
+
this._config = {
|
|
122
|
+
...options.config
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The entire input value.
|
|
128
|
+
*/
|
|
129
|
+
get value(): string {
|
|
130
|
+
return this._value;
|
|
131
|
+
}
|
|
132
|
+
set value(newInput: string) {
|
|
133
|
+
this._value = newInput;
|
|
134
|
+
this._valueChanged.emit(newInput);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* A signal emitting when the value has changed.
|
|
139
|
+
*/
|
|
140
|
+
get valueChanged(): ISignal<IInputModel, string> {
|
|
141
|
+
return this._valueChanged;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The cursor position in the input.
|
|
146
|
+
*/
|
|
147
|
+
get cursorIndex(): number | null {
|
|
148
|
+
return this._cursorIndex;
|
|
149
|
+
}
|
|
150
|
+
set cursorIndex(newIndex: number | null) {
|
|
151
|
+
if (newIndex === null || newIndex > this._value.length) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this._cursorIndex = newIndex;
|
|
155
|
+
this._cursorIndexChanged.emit(newIndex);
|
|
156
|
+
|
|
157
|
+
if (this._cursorIndex === null) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const currentWord = Private.getCurrentWord(this._value, this._cursorIndex);
|
|
161
|
+
if (currentWord !== this._currentWord) {
|
|
162
|
+
this._currentWord = currentWord;
|
|
163
|
+
this._currentWordChanged.emit(this._currentWord);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A signal emitting when the cursor position has changed.
|
|
169
|
+
*/
|
|
170
|
+
get cursorIndexChanged(): ISignal<IInputModel, number | null> {
|
|
171
|
+
return this._cursorIndexChanged;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The current word behind the user's cursor, space-separated.
|
|
176
|
+
*/
|
|
177
|
+
get currentWord(): string | null {
|
|
178
|
+
return this._currentWord;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* A signal emitting when the current word has changed.
|
|
183
|
+
*/
|
|
184
|
+
get currentWordChanged(): ISignal<IInputModel, string | null> {
|
|
185
|
+
return this._currentWordChanged;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get the active cell manager.
|
|
190
|
+
*/
|
|
191
|
+
get activeCellManager(): IActiveCellManager | null {
|
|
192
|
+
return this._activeCellManager;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get the selection watcher.
|
|
197
|
+
*/
|
|
198
|
+
get selectionWatcher(): ISelectionWatcher | null {
|
|
199
|
+
return this._selectionWatcher;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The input configuration.
|
|
204
|
+
*/
|
|
205
|
+
get config(): InputModel.IConfig {
|
|
206
|
+
return this._config;
|
|
207
|
+
}
|
|
208
|
+
set config(value: Partial<InputModel.IConfig>) {
|
|
209
|
+
this._config = { ...this._config, ...value };
|
|
210
|
+
this._configChanged.emit(this._config);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* A signal emitting when the configuration is updated.
|
|
215
|
+
*/
|
|
216
|
+
get configChanged(): ISignal<IInputModel, InputModel.IConfig> {
|
|
217
|
+
return this._configChanged;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Function to request the focus on the input of the chat.
|
|
222
|
+
*/
|
|
223
|
+
focus(): void {
|
|
224
|
+
this._focusInputSignal.emit();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* A signal emitting when the focus is requested on the input.
|
|
229
|
+
*/
|
|
230
|
+
get focusInputSignal(): ISignal<IInputModel, void> {
|
|
231
|
+
return this._focusInputSignal;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The attachments list.
|
|
236
|
+
*/
|
|
237
|
+
get attachments(): IAttachment[] {
|
|
238
|
+
return this._attachments;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Add attachment to send with next message.
|
|
243
|
+
*/
|
|
244
|
+
addAttachment = (attachment: IAttachment): void => {
|
|
245
|
+
const duplicateAttachment = this._attachments.find(
|
|
246
|
+
att => att.type === attachment.type && att.value === attachment.value
|
|
247
|
+
);
|
|
248
|
+
if (duplicateAttachment) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this._attachments.push(attachment);
|
|
253
|
+
this._attachmentsChanged.emit([...this._attachments]);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Remove attachment to be sent.
|
|
258
|
+
*/
|
|
259
|
+
removeAttachment = (attachment: IAttachment): void => {
|
|
260
|
+
const attachmentIndex = this._attachments.findIndex(
|
|
261
|
+
att => att.type === attachment.type && att.value === attachment.value
|
|
262
|
+
);
|
|
263
|
+
if (attachmentIndex === -1) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this._attachments.splice(attachmentIndex, 1);
|
|
268
|
+
this._attachmentsChanged.emit([...this._attachments]);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Update attachments.
|
|
273
|
+
*/
|
|
274
|
+
clearAttachments = (): void => {
|
|
275
|
+
this._attachments = [];
|
|
276
|
+
this._attachmentsChanged.emit([]);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* A signal emitting when the input attachments changed.
|
|
281
|
+
*/
|
|
282
|
+
get attachmentsChanged(): ISignal<IInputModel, IAttachment[]> {
|
|
283
|
+
return this._attachmentsChanged;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Replace the current word in the input with a new one.
|
|
288
|
+
*/
|
|
289
|
+
replaceCurrentWord(newWord: string): void {
|
|
290
|
+
if (this.cursorIndex === null) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const [start, end] = Private.getCurrentWordBoundaries(
|
|
294
|
+
this.value,
|
|
295
|
+
this.cursorIndex
|
|
296
|
+
);
|
|
297
|
+
this.value = this.value.slice(0, start) + newWord + this.value.slice(end);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Dispose the input model.
|
|
302
|
+
*/
|
|
303
|
+
dispose(): void {
|
|
304
|
+
if (this.isDisposed) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
this._isDisposed = true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Whether the input model is disposed.
|
|
312
|
+
*/
|
|
313
|
+
get isDisposed(): boolean {
|
|
314
|
+
return this._isDisposed;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private _value: string;
|
|
318
|
+
private _cursorIndex: number | null = null;
|
|
319
|
+
private _currentWord: string | null = null;
|
|
320
|
+
private _attachments: IAttachment[];
|
|
321
|
+
private _activeCellManager: IActiveCellManager | null;
|
|
322
|
+
private _selectionWatcher: ISelectionWatcher | null;
|
|
323
|
+
private _config: InputModel.IConfig;
|
|
324
|
+
private _valueChanged = new Signal<IInputModel, string>(this);
|
|
325
|
+
private _cursorIndexChanged = new Signal<IInputModel, number | null>(this);
|
|
326
|
+
private _currentWordChanged = new Signal<IInputModel, string | null>(this);
|
|
327
|
+
private _configChanged = new Signal<IInputModel, InputModel.IConfig>(this);
|
|
328
|
+
private _focusInputSignal = new Signal<InputModel, void>(this);
|
|
329
|
+
private _attachmentsChanged = new Signal<InputModel, IAttachment[]>(this);
|
|
330
|
+
private _isDisposed = false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export namespace InputModel {
|
|
334
|
+
export interface IOptions {
|
|
335
|
+
/**
|
|
336
|
+
* The initial value of the input.
|
|
337
|
+
*/
|
|
338
|
+
value?: string;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The initial attachments.
|
|
342
|
+
*/
|
|
343
|
+
attachments?: IAttachment[];
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* The current cursor index.
|
|
347
|
+
* This refers to the index of the character in front of the cursor.
|
|
348
|
+
*/
|
|
349
|
+
cursorIndex?: number;
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The configuration for the input component.
|
|
353
|
+
*/
|
|
354
|
+
config?: IConfig;
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Active cell manager.
|
|
358
|
+
*/
|
|
359
|
+
activeCellManager?: IActiveCellManager | null;
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Selection watcher.
|
|
363
|
+
*/
|
|
364
|
+
selectionWatcher?: ISelectionWatcher | null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export interface IConfig {
|
|
368
|
+
/**
|
|
369
|
+
* Whether to send a message via Shift-Enter instead of Enter.
|
|
370
|
+
*/
|
|
371
|
+
sendWithShiftEnter?: boolean;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
namespace Private {
|
|
376
|
+
export function getCurrentWordBoundaries(
|
|
377
|
+
input: string,
|
|
378
|
+
cursorIndex: number
|
|
379
|
+
): [number, number] {
|
|
380
|
+
let start = cursorIndex;
|
|
381
|
+
let end = cursorIndex;
|
|
382
|
+
const n = input.length;
|
|
383
|
+
|
|
384
|
+
while (start > 0 && !WHITESPACE.has(input[start - 1])) {
|
|
385
|
+
start--;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
while (end < n && !WHITESPACE.has(input[end])) {
|
|
389
|
+
end++;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return [start, end];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Gets the current (space-separated) word around the user's cursor. The current
|
|
397
|
+
* word is used to generate a list of matching chat commands.
|
|
398
|
+
*/
|
|
399
|
+
export function getCurrentWord(
|
|
400
|
+
input: string,
|
|
401
|
+
cursorIndex: number
|
|
402
|
+
): string | null {
|
|
403
|
+
const [start, end] = getCurrentWordBoundaries(input, cursorIndex);
|
|
404
|
+
return input.slice(start, end);
|
|
405
|
+
}
|
|
406
|
+
}
|