@jupyter/chat 0.23.0-alpha.2 → 0.23.0-alpha.4
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/__tests__/input-model.spec.d.ts +9 -0
- package/lib/__tests__/input-model.spec.js +102 -0
- package/lib/__tests__/model.spec.js +17 -3
- package/lib/__tests__/multichat-panel.spec.js +78 -27
- package/lib/components/messages/message.js +15 -8
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/input-model.d.ts +50 -2
- package/lib/input-model.js +57 -1
- package/lib/model.d.ts +14 -0
- package/lib/model.js +4 -1
- package/lib/tokens.d.ts +21 -5
- package/lib/types.d.ts +1 -4
- package/lib/utils.d.ts +11 -0
- package/lib/utils.js +42 -0
- package/lib/widgets/multichat-panel.d.ts +20 -20
- package/lib/widgets/multichat-panel.js +33 -48
- package/package.json +3 -1
- package/src/__tests__/input-model.spec.ts +129 -0
- package/src/__tests__/model.spec.ts +18 -0
- package/src/__tests__/multichat-panel.spec.ts +93 -37
- package/src/components/messages/message.tsx +21 -11
- package/src/index.ts +1 -0
- package/src/input-model.ts +106 -4
- package/src/model.ts +19 -1
- package/src/tokens.ts +21 -5
- package/src/types.ts +6 -4
- package/src/utils.ts +59 -0
- package/src/widgets/multichat-panel.tsx +54 -66
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { ToolbarRegistry } from '@jupyterlab/apputils';
|
|
7
7
|
import { RenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
8
|
+
import { ObservableList } from '@jupyterlab/observables';
|
|
9
|
+
import { ToolbarButton, PanelWithToolbar } from '@jupyterlab/ui-components';
|
|
8
10
|
import { Widget } from '@lumino/widgets';
|
|
9
11
|
|
|
10
12
|
import { IChatPlaceholderFactory } from '../tokens';
|
|
11
13
|
import { MultiChatPanel } from '../widgets/multichat-panel';
|
|
12
14
|
import { defaultPlaceholder, Placeholder } from '../widgets/placeholder';
|
|
15
|
+
import { MockChatModel } from './mocks';
|
|
13
16
|
|
|
14
17
|
describe('MultiChatPanel', () => {
|
|
15
18
|
let rmRegistry: RenderMimeRegistry;
|
|
@@ -79,40 +82,6 @@ describe('MultiChatPanel', () => {
|
|
|
79
82
|
});
|
|
80
83
|
});
|
|
81
84
|
|
|
82
|
-
describe('openInMain', () => {
|
|
83
|
-
it('should not dispose the model when moving to main area', async () => {
|
|
84
|
-
const { MockChatModel } = await import('./mocks');
|
|
85
|
-
const model = new MockChatModel();
|
|
86
|
-
const openInMain = jest.fn().mockResolvedValue(true);
|
|
87
|
-
|
|
88
|
-
const panel = new MultiChatPanel({ rmRegistry, openInMain });
|
|
89
|
-
panel.open({ model, displayName: 'test-chat' });
|
|
90
|
-
|
|
91
|
-
// Find the 'moveMain' toolbar button by name and invoke its click handler directly.
|
|
92
|
-
// ToolbarButton stores the onClick handler independently of DOM/React rendering,
|
|
93
|
-
// so this works without attaching the widget to the document.
|
|
94
|
-
const toolbar = panel.current!.toolbar;
|
|
95
|
-
const names = Array.from(toolbar.names());
|
|
96
|
-
const moveMainIdx = names.indexOf('moveMain');
|
|
97
|
-
expect(moveMainIdx).toBeGreaterThan(-1);
|
|
98
|
-
|
|
99
|
-
const moveButton = (toolbar.layout as any).widgets[
|
|
100
|
-
moveMainIdx
|
|
101
|
-
] as ToolbarButton;
|
|
102
|
-
|
|
103
|
-
moveButton.onClick();
|
|
104
|
-
// Wait for the async onClick handler: openInMain resolves, then onClose is called.
|
|
105
|
-
await Promise.resolve();
|
|
106
|
-
|
|
107
|
-
expect(openInMain).toHaveBeenCalledWith(model.name);
|
|
108
|
-
expect(model.isDisposed).toBe(false);
|
|
109
|
-
expect(panel.getLoadedModel('test-chat')).toBeUndefined();
|
|
110
|
-
|
|
111
|
-
model.dispose();
|
|
112
|
-
panel.dispose();
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
85
|
describe('placeholderFactory', () => {
|
|
117
86
|
it('should use defaultPlaceholder when no factory is provided', () => {
|
|
118
87
|
const panel = new MultiChatPanel({ rmRegistry });
|
|
@@ -207,7 +176,7 @@ describe('MultiChatPanel', () => {
|
|
|
207
176
|
panel.dispose();
|
|
208
177
|
});
|
|
209
178
|
|
|
210
|
-
it('should call factory.create again when the placeholder is re-added after closing a chat',
|
|
179
|
+
it('should call factory.create again when the placeholder is re-added after closing a chat', () => {
|
|
211
180
|
const factory: IChatPlaceholderFactory = {
|
|
212
181
|
create: jest.fn().mockReturnValue(new Widget())
|
|
213
182
|
};
|
|
@@ -219,7 +188,6 @@ describe('MultiChatPanel', () => {
|
|
|
219
188
|
});
|
|
220
189
|
|
|
221
190
|
// Simulate opening and closing a chat to trigger _addPlaceholder again.
|
|
222
|
-
const { MockChatModel } = await import('./mocks');
|
|
223
191
|
const model = new MockChatModel();
|
|
224
192
|
panel.open({ model, displayName: 'test-chat' });
|
|
225
193
|
panel.unsetLoadedModel('test-chat');
|
|
@@ -240,7 +208,95 @@ describe('MultiChatPanel', () => {
|
|
|
240
208
|
'.jp-Toolbar-responsive-opener'
|
|
241
209
|
);
|
|
242
210
|
expect(opener).not.toBeNull();
|
|
211
|
+
panel.dispose();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('chatToolbarFactory', () => {
|
|
216
|
+
const makeFactory = (items: ToolbarRegistry.IToolbarItem[]) =>
|
|
217
|
+
jest.fn().mockReturnValue(new ObservableList({ values: items }));
|
|
218
|
+
|
|
219
|
+
it('should call factory with the panel when a chat is opened', () => {
|
|
220
|
+
const factory = makeFactory([{ name: 'myButton', widget: new Widget() }]);
|
|
221
|
+
const panel = new MultiChatPanel({
|
|
222
|
+
rmRegistry,
|
|
223
|
+
chatToolbarFactory: factory
|
|
224
|
+
});
|
|
225
|
+
const model = new MockChatModel();
|
|
226
|
+
const chatWidget = panel.open({ model, displayName: 'test' });
|
|
227
|
+
expect(factory).toHaveBeenCalledTimes(1);
|
|
228
|
+
expect(factory.mock.calls[0][0].widget).toBe(chatWidget);
|
|
229
|
+
panel.dispose();
|
|
230
|
+
});
|
|
243
231
|
|
|
232
|
+
it('should add the created widget to the chat toolbar', () => {
|
|
233
|
+
const panel = new MultiChatPanel({
|
|
234
|
+
rmRegistry,
|
|
235
|
+
chatToolbarFactory: makeFactory([
|
|
236
|
+
{ name: 'myButton', widget: new Widget() }
|
|
237
|
+
])
|
|
238
|
+
});
|
|
239
|
+
const model = new MockChatModel();
|
|
240
|
+
panel.open({ model, displayName: 'test' });
|
|
241
|
+
const toolbar = (panel.current as unknown as PanelWithToolbar).toolbar;
|
|
242
|
+
expect(Array.from(toolbar.names())).toContain('myButton');
|
|
243
|
+
panel.dispose();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should insert the item before "close"', () => {
|
|
247
|
+
const panel = new MultiChatPanel({
|
|
248
|
+
rmRegistry,
|
|
249
|
+
chatToolbarFactory: makeFactory([
|
|
250
|
+
{ name: 'myButton', widget: new Widget() }
|
|
251
|
+
])
|
|
252
|
+
});
|
|
253
|
+
const model = new MockChatModel();
|
|
254
|
+
panel.open({ model, displayName: 'test' });
|
|
255
|
+
const toolbar = (panel.current as unknown as PanelWithToolbar).toolbar;
|
|
256
|
+
const names = Array.from(toolbar.names());
|
|
257
|
+
expect(names.indexOf('myButton')).toBeLessThan(names.indexOf('close'));
|
|
258
|
+
panel.dispose();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should add all items when factory returns multiple items', () => {
|
|
262
|
+
const panel = new MultiChatPanel({
|
|
263
|
+
rmRegistry,
|
|
264
|
+
chatToolbarFactory: makeFactory([
|
|
265
|
+
{ name: 'item1', widget: new Widget() },
|
|
266
|
+
{ name: 'item2', widget: new Widget() }
|
|
267
|
+
])
|
|
268
|
+
});
|
|
269
|
+
const model = new MockChatModel();
|
|
270
|
+
panel.open({ model, displayName: 'test' });
|
|
271
|
+
const toolbar = (panel.current as unknown as PanelWithToolbar).toolbar;
|
|
272
|
+
const names = Array.from(toolbar.names());
|
|
273
|
+
expect(names).toContain('item1');
|
|
274
|
+
expect(names).toContain('item2');
|
|
275
|
+
panel.dispose();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should call factory again for each newly opened chat', () => {
|
|
279
|
+
const factory = jest.fn().mockImplementation(
|
|
280
|
+
() =>
|
|
281
|
+
new ObservableList<ToolbarRegistry.IToolbarItem>({
|
|
282
|
+
values: [{ name: 'myButton', widget: new Widget() }]
|
|
283
|
+
})
|
|
284
|
+
);
|
|
285
|
+
const panel = new MultiChatPanel({
|
|
286
|
+
rmRegistry,
|
|
287
|
+
chatToolbarFactory: factory
|
|
288
|
+
});
|
|
289
|
+
const model1 = new MockChatModel();
|
|
290
|
+
panel.open({ model: model1, displayName: 'chat1' });
|
|
291
|
+
panel.unsetLoadedModel('chat1');
|
|
292
|
+
|
|
293
|
+
const model2 = new MockChatModel();
|
|
294
|
+
panel.open({ model: model2, displayName: 'chat2' });
|
|
295
|
+
|
|
296
|
+
expect(factory).toHaveBeenCalledTimes(2);
|
|
297
|
+
expect(factory.mock.results[0].value).not.toBe(
|
|
298
|
+
factory.mock.results[1].value
|
|
299
|
+
);
|
|
244
300
|
panel.dispose();
|
|
245
301
|
});
|
|
246
302
|
});
|
|
@@ -10,8 +10,8 @@ import { MessageRenderer } from './message-renderer';
|
|
|
10
10
|
import { AttachmentPreviewList } from '../attachments';
|
|
11
11
|
import { ChatInput } from '../input';
|
|
12
12
|
import { useChatContext } from '../../context';
|
|
13
|
-
import {
|
|
14
|
-
import { IMessageContent, IMessage } from '../../types';
|
|
13
|
+
import { InputModel } from '../../input-model';
|
|
14
|
+
import { IMessageContent, IMessage, INewMessage } from '../../types';
|
|
15
15
|
import { replaceSpanToMention } from '../../utils';
|
|
16
16
|
|
|
17
17
|
export const MESSAGE_CONTAINER_CLASS = 'jp-chat-message-container';
|
|
@@ -96,8 +96,8 @@ export function ChatMessageBase(props: ChatMessageProps): JSX.Element {
|
|
|
96
96
|
});
|
|
97
97
|
const inputModel = new InputModel({
|
|
98
98
|
chatContext: model.createChatContext(),
|
|
99
|
-
onSend: (
|
|
100
|
-
updateMessage(message.id,
|
|
99
|
+
onSend: (newMessage: INewMessage) =>
|
|
100
|
+
updateMessage(message.id, newMessage),
|
|
101
101
|
onCancel: () => cancelEdition(),
|
|
102
102
|
value: body,
|
|
103
103
|
activeCellManager: model.activeCellManager,
|
|
@@ -121,17 +121,27 @@ export function ChatMessageBase(props: ChatMessageProps): JSX.Element {
|
|
|
121
121
|
|
|
122
122
|
// Update the content of the message.
|
|
123
123
|
const updateMessage = useCallback(
|
|
124
|
-
(id: string,
|
|
125
|
-
|
|
124
|
+
(id: string, newMessage: INewMessage): void => {
|
|
125
|
+
const inputModel = model.getEditionModel(id);
|
|
126
|
+
if (!canEdit || !inputModel || !model.updateMessage) {
|
|
127
|
+
setEdit(false);
|
|
126
128
|
return;
|
|
127
129
|
}
|
|
130
|
+
|
|
128
131
|
// Update the message
|
|
129
132
|
const updatedMessage = { ...message };
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
if (newMessage.body) {
|
|
134
|
+
updatedMessage.body = newMessage.body;
|
|
135
|
+
}
|
|
136
|
+
if (newMessage.attachments) {
|
|
137
|
+
updatedMessage.attachments = newMessage.attachments;
|
|
138
|
+
}
|
|
139
|
+
if (newMessage.mentions) {
|
|
140
|
+
updatedMessage.mentions = newMessage.mentions;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
model.updateMessage(id, updatedMessage);
|
|
144
|
+
inputModel.dispose();
|
|
135
145
|
setEdit(false);
|
|
136
146
|
},
|
|
137
147
|
[model, message, canEdit]
|
package/src/index.ts
CHANGED
package/src/input-model.ts
CHANGED
|
@@ -10,7 +10,13 @@ import { ISignal, Signal } from '@lumino/signaling';
|
|
|
10
10
|
import { IActiveCellManager } from './active-cell-manager';
|
|
11
11
|
import { ISelectionWatcher } from './selection-watcher';
|
|
12
12
|
import { IChatContext } from './model';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
IAttachment,
|
|
15
|
+
IMessageMetadata,
|
|
16
|
+
INewMessage,
|
|
17
|
+
INotebookAttachment,
|
|
18
|
+
IUser
|
|
19
|
+
} from './types';
|
|
14
20
|
|
|
15
21
|
/**
|
|
16
22
|
* The chat input interface.
|
|
@@ -152,6 +158,31 @@ export interface IInputModel extends IDisposable {
|
|
|
152
158
|
*/
|
|
153
159
|
clearMentions(): void;
|
|
154
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Get the metadata to attach to the next message to send.
|
|
163
|
+
*/
|
|
164
|
+
getMetadata(): IMessageMetadata;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Merge a patch into the metadata to attach to the next message to send.
|
|
168
|
+
*
|
|
169
|
+
* This is a *shallow* merge: each key in `patch` replaces the existing value
|
|
170
|
+
* at that key wholesale, rather than being merged recursively. To change a
|
|
171
|
+
* nested field, pass the whole new value for its top-level key. The patch is
|
|
172
|
+
* deep-copied, so mutating it afterwards won't affect the stored metadata.
|
|
173
|
+
*/
|
|
174
|
+
updateMetadata(patch: IMessageMetadata): void;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Clear the metadata.
|
|
178
|
+
*/
|
|
179
|
+
clearMetadata(): void;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* A signal emitting when the metadata has changed.
|
|
183
|
+
*/
|
|
184
|
+
readonly metadataChanged?: ISignal<IInputModel, IMessageMetadata>;
|
|
185
|
+
|
|
155
186
|
/**
|
|
156
187
|
* A signal emitting when disposing of the model.
|
|
157
188
|
*/
|
|
@@ -169,6 +200,7 @@ export class InputModel implements IInputModel {
|
|
|
169
200
|
this._value = options.value || '';
|
|
170
201
|
this._attachments = options.attachments || [];
|
|
171
202
|
this._mentions = options.mentions || [];
|
|
203
|
+
this._metadata = options.metadata || {};
|
|
172
204
|
this.cursorIndex = options.cursorIndex || this.value.length;
|
|
173
205
|
this._activeCellManager = options.activeCellManager ?? null;
|
|
174
206
|
this._selectionWatcher = options.selectionWatcher ?? null;
|
|
@@ -194,8 +226,32 @@ export class InputModel implements IInputModel {
|
|
|
194
226
|
* Function to send a message.
|
|
195
227
|
*/
|
|
196
228
|
send = (input: string): void => {
|
|
197
|
-
|
|
229
|
+
const message: INewMessage = {
|
|
230
|
+
body: input
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Add the attachments
|
|
234
|
+
if (this.attachments.length) {
|
|
235
|
+
message.attachments = [...this.attachments];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Add the mentions
|
|
239
|
+
if (this.mentions.length) {
|
|
240
|
+
message.mentions = [...this.mentions];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Add the metadata
|
|
244
|
+
if (Object.keys(this._metadata).length) {
|
|
245
|
+
message.metadata = { ...this._metadata };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Send the message
|
|
249
|
+
this._onSend(message);
|
|
250
|
+
|
|
251
|
+
// Clear the input
|
|
198
252
|
this.value = '';
|
|
253
|
+
this.clearAttachments();
|
|
254
|
+
this.clearMentions();
|
|
199
255
|
};
|
|
200
256
|
|
|
201
257
|
/**
|
|
@@ -460,6 +516,45 @@ export class InputModel implements IInputModel {
|
|
|
460
516
|
this._mentions = [];
|
|
461
517
|
};
|
|
462
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Get the metadata to attach to the next message to send.
|
|
521
|
+
*/
|
|
522
|
+
getMetadata(): IMessageMetadata {
|
|
523
|
+
return this._metadata;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Merge a patch into the metadata to attach to the next message to send.
|
|
528
|
+
*
|
|
529
|
+
* This is a *shallow* merge: each key in `patch` replaces the existing value
|
|
530
|
+
* at that key wholesale, rather than being merged recursively. To change a
|
|
531
|
+
* nested field, pass the whole new value for its top-level key. The patch is
|
|
532
|
+
* deep-copied, so mutating it afterwards won't affect the stored metadata.
|
|
533
|
+
*/
|
|
534
|
+
updateMetadata = (patch: IMessageMetadata): void => {
|
|
535
|
+
// Deep-copy the patch so a caller mutating a nested field afterwards can't
|
|
536
|
+
// reach into the stored metadata. `this._metadata` only ever holds values
|
|
537
|
+
// that were themselves deep-copied on the way in, so a shallow spread of it
|
|
538
|
+
// is safe.
|
|
539
|
+
this._metadata = { ...this._metadata, ...structuredClone(patch) };
|
|
540
|
+
this._metadataChanged.emit(this._metadata);
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Clear the metadata.
|
|
545
|
+
*/
|
|
546
|
+
clearMetadata = (): void => {
|
|
547
|
+
this._metadata = {};
|
|
548
|
+
this._metadataChanged.emit(this._metadata);
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* A signal emitting when the metadata has changed.
|
|
553
|
+
*/
|
|
554
|
+
get metadataChanged(): ISignal<IInputModel, IMessageMetadata> {
|
|
555
|
+
return this._metadataChanged;
|
|
556
|
+
}
|
|
557
|
+
|
|
463
558
|
/**
|
|
464
559
|
* Dispose the input model.
|
|
465
560
|
*/
|
|
@@ -486,13 +581,14 @@ export class InputModel implements IInputModel {
|
|
|
486
581
|
}
|
|
487
582
|
|
|
488
583
|
private _id: string;
|
|
489
|
-
private _onSend: (
|
|
584
|
+
private _onSend: (message: INewMessage) => void;
|
|
490
585
|
private _chatContext?: IChatContext;
|
|
491
586
|
private _value: string;
|
|
492
587
|
private _cursorIndex: number | null = null;
|
|
493
588
|
private _currentWord: string | null = null;
|
|
494
589
|
private _attachments: IAttachment[];
|
|
495
590
|
private _mentions: IUser[];
|
|
591
|
+
private _metadata: IMessageMetadata;
|
|
496
592
|
private _activeCellManager: IActiveCellManager | null;
|
|
497
593
|
private _selectionWatcher: ISelectionWatcher | null;
|
|
498
594
|
private _documentManager: IDocumentManager | null;
|
|
@@ -503,6 +599,7 @@ export class InputModel implements IInputModel {
|
|
|
503
599
|
private _configChanged = new Signal<IInputModel, InputModel.IConfig>(this);
|
|
504
600
|
private _focusInputSignal = new Signal<InputModel, void>(this);
|
|
505
601
|
private _attachmentsChanged = new Signal<InputModel, IAttachment[]>(this);
|
|
602
|
+
private _metadataChanged = new Signal<InputModel, IMessageMetadata>(this);
|
|
506
603
|
private _onDisposed = new Signal<InputModel, void>(this);
|
|
507
604
|
private _isDisposed = false;
|
|
508
605
|
}
|
|
@@ -519,7 +616,7 @@ export namespace InputModel {
|
|
|
519
616
|
* @param content - the content of the message.
|
|
520
617
|
* @param model - the model of the input sending the message.
|
|
521
618
|
*/
|
|
522
|
-
onSend: (
|
|
619
|
+
onSend: (message: INewMessage) => void;
|
|
523
620
|
|
|
524
621
|
/**
|
|
525
622
|
* Function that should cancel the message edition.
|
|
@@ -541,6 +638,11 @@ export namespace InputModel {
|
|
|
541
638
|
*/
|
|
542
639
|
mentions?: IUser[];
|
|
543
640
|
|
|
641
|
+
/**
|
|
642
|
+
* The initial metadata.
|
|
643
|
+
*/
|
|
644
|
+
metadata?: IMessageMetadata;
|
|
645
|
+
|
|
544
646
|
/**
|
|
545
647
|
* The current cursor index.
|
|
546
648
|
* This refers to the index of the character in front of the cursor.
|
package/src/model.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
nullTranslator,
|
|
10
10
|
TranslationBundle
|
|
11
11
|
} from '@jupyterlab/translation';
|
|
12
|
+
import type { IAwareness } from '@jupyter/ydoc';
|
|
12
13
|
import { ArrayExt } from '@lumino/algorithm';
|
|
13
14
|
import { CommandRegistry } from '@lumino/commands';
|
|
14
15
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
@@ -53,6 +54,14 @@ export interface IChatModel extends IDisposable {
|
|
|
53
54
|
*/
|
|
54
55
|
readonly ready: Promise<void>;
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The awareness channel of the underlying shared document, when the chat is
|
|
59
|
+
* backed by one. Lets extensions read collaborative state attached to the
|
|
60
|
+
* chat (e.g. session information published by AI personas) through a
|
|
61
|
+
* supported API instead of reaching into a concrete implementation.
|
|
62
|
+
*/
|
|
63
|
+
readonly awareness?: IAwareness;
|
|
64
|
+
|
|
56
65
|
/**
|
|
57
66
|
* The indexes list of the messages currently in the viewport.
|
|
58
67
|
*/
|
|
@@ -264,7 +273,7 @@ export abstract class AbstractChatModel implements IChatModel {
|
|
|
264
273
|
config: {
|
|
265
274
|
sendWithShiftEnter: config.sendWithShiftEnter
|
|
266
275
|
},
|
|
267
|
-
onSend:
|
|
276
|
+
onSend: this.sendMessage.bind(this)
|
|
268
277
|
});
|
|
269
278
|
|
|
270
279
|
this._commands = options.commands;
|
|
@@ -894,6 +903,11 @@ export interface IChatContext {
|
|
|
894
903
|
* Current user connected with the chat panel
|
|
895
904
|
*/
|
|
896
905
|
readonly user: IUser | undefined;
|
|
906
|
+
/**
|
|
907
|
+
* The awareness channel of the underlying shared document, when the chat is
|
|
908
|
+
* backed by one.
|
|
909
|
+
*/
|
|
910
|
+
readonly awareness?: IAwareness;
|
|
897
911
|
}
|
|
898
912
|
|
|
899
913
|
/**
|
|
@@ -917,6 +931,10 @@ export abstract class AbstractChatContext implements IChatContext {
|
|
|
917
931
|
return this._model?.user;
|
|
918
932
|
}
|
|
919
933
|
|
|
934
|
+
get awareness(): IAwareness | undefined {
|
|
935
|
+
return this._model.awareness;
|
|
936
|
+
}
|
|
937
|
+
|
|
920
938
|
/**
|
|
921
939
|
* ABSTRACT: Should return a list of users who have connected to this chat.
|
|
922
940
|
*/
|
package/src/tokens.ts
CHANGED
|
@@ -3,24 +3,40 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { IWidgetTracker
|
|
6
|
+
import { IWidgetTracker } from '@jupyterlab/apputils';
|
|
7
7
|
import { Token } from '@lumino/coreutils';
|
|
8
8
|
import { Widget } from '@lumino/widgets';
|
|
9
9
|
|
|
10
10
|
import { ChatWidget, Placeholder } from './widgets';
|
|
11
11
|
import { IChatModel } from './model';
|
|
12
|
+
import { ChatArea } from './types';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* The main area
|
|
15
|
+
* The interface for any widget displaying a chat (main area or side panel).
|
|
15
16
|
*/
|
|
16
|
-
export
|
|
17
|
+
export interface IChatPanel extends Widget {
|
|
18
|
+
/**
|
|
19
|
+
* The chat widget embedded in the panel.
|
|
20
|
+
*/
|
|
21
|
+
widget: ChatWidget;
|
|
22
|
+
/**
|
|
23
|
+
* The model of the chat widget.
|
|
24
|
+
*/
|
|
17
25
|
model: IChatModel;
|
|
18
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The area of the panel.
|
|
28
|
+
*/
|
|
29
|
+
area: ChatArea;
|
|
30
|
+
/**
|
|
31
|
+
* The chat panel toolbar.
|
|
32
|
+
*/
|
|
33
|
+
toolbar: Widget;
|
|
34
|
+
}
|
|
19
35
|
|
|
20
36
|
/**
|
|
21
37
|
* the chat tracker type.
|
|
22
38
|
*/
|
|
23
|
-
export type IChatTracker = IWidgetTracker<
|
|
39
|
+
export type IChatTracker = IWidgetTracker<IChatPanel>;
|
|
24
40
|
|
|
25
41
|
/**
|
|
26
42
|
* A chat tracker token.
|
package/src/types.ts
CHANGED
|
@@ -174,10 +174,12 @@ export interface IChatHistory {
|
|
|
174
174
|
/**
|
|
175
175
|
* The content of a new message.
|
|
176
176
|
*/
|
|
177
|
-
export
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
export type INewMessage<T = IUser, U = IAttachment> = Partial<
|
|
178
|
+
Pick<
|
|
179
|
+
IMessageContent<T, U>,
|
|
180
|
+
'body' | 'attachments' | 'mentions' | 'metadata' | 'mime_model' | 'sender'
|
|
181
|
+
>
|
|
182
|
+
>;
|
|
181
183
|
|
|
182
184
|
/**
|
|
183
185
|
* The attachment type. Jupyter Chat allows for two types of attachments
|
package/src/utils.ts
CHANGED
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { CommandToolbarButton, ToolbarRegistry } from '@jupyterlab/apputils';
|
|
6
7
|
import { CodeEditor } from '@jupyterlab/codeeditor';
|
|
7
8
|
import { CodeMirrorEditor } from '@jupyterlab/codemirror';
|
|
8
9
|
import { DocumentWidget } from '@jupyterlab/docregistry';
|
|
9
10
|
import { FileEditor } from '@jupyterlab/fileeditor';
|
|
10
11
|
import { Notebook } from '@jupyterlab/notebook';
|
|
12
|
+
import { IObservableList, ObservableList } from '@jupyterlab/observables';
|
|
13
|
+
import { CommandRegistry } from '@lumino/commands';
|
|
11
14
|
import { Widget } from '@lumino/widgets';
|
|
12
15
|
|
|
16
|
+
import { IChatPanel } from './tokens';
|
|
13
17
|
import { IUser } from './types';
|
|
14
18
|
|
|
15
19
|
const MENTION_CLASS = 'jp-chat-mention';
|
|
@@ -81,3 +85,58 @@ export function replaceSpanToMention(content: string, user: IUser): string {
|
|
|
81
85
|
const regex = new RegExp(mentionEl, 'g');
|
|
82
86
|
return content.replace(regex, mention);
|
|
83
87
|
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Wraps a toolbar factory so that every CommandToolbarButton it creates
|
|
91
|
+
* automatically receives the panel's area as an arg.
|
|
92
|
+
* This lets commands branch on `args.area` without each plugin having to
|
|
93
|
+
* register a per-item toolbarRegistry.addFactory call.
|
|
94
|
+
*/
|
|
95
|
+
export function injectAreaArg(
|
|
96
|
+
baseFactory: (
|
|
97
|
+
panel: IChatPanel
|
|
98
|
+
) => IObservableList<ToolbarRegistry.IToolbarItem>,
|
|
99
|
+
commands: CommandRegistry
|
|
100
|
+
): (panel: IChatPanel) => IObservableList<ToolbarRegistry.IToolbarItem> {
|
|
101
|
+
return (panel: IChatPanel) => {
|
|
102
|
+
const base = baseFactory(panel);
|
|
103
|
+
|
|
104
|
+
const inject = (
|
|
105
|
+
item: ToolbarRegistry.IToolbarItem
|
|
106
|
+
): ToolbarRegistry.IToolbarItem => {
|
|
107
|
+
const { widget } = item;
|
|
108
|
+
if (!(widget instanceof CommandToolbarButton)) {
|
|
109
|
+
return item;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
name: item.name,
|
|
113
|
+
widget: new CommandToolbarButton({
|
|
114
|
+
commands,
|
|
115
|
+
id: widget.commandId,
|
|
116
|
+
args: { area: panel.area }
|
|
117
|
+
})
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const wrapped = new ObservableList<ToolbarRegistry.IToolbarItem>({
|
|
122
|
+
values: Array.from({ length: base.length }, (_, i) => inject(base.get(i)))
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
base.changed.connect((_, change) => {
|
|
126
|
+
if (change.type === 'add') {
|
|
127
|
+
wrapped.insertAll(change.newIndex, change.newValues.map(inject));
|
|
128
|
+
} else if (change.type === 'remove') {
|
|
129
|
+
wrapped.removeRange(
|
|
130
|
+
change.oldIndex,
|
|
131
|
+
change.oldIndex + change.oldValues.length
|
|
132
|
+
);
|
|
133
|
+
} else if (change.type === 'set') {
|
|
134
|
+
change.newValues.forEach((item, i) =>
|
|
135
|
+
wrapped.set(change.newIndex + i, inject(item))
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return wrapped;
|
|
141
|
+
};
|
|
142
|
+
}
|