@jupyter/chat 0.21.0 → 0.22.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 (49) hide show
  1. package/lib/__tests__/chat-body-placeholder.spec.d.ts +1 -0
  2. package/lib/__tests__/chat-body-placeholder.spec.js +54 -0
  3. package/lib/__tests__/multichat-panel.spec.d.ts +1 -0
  4. package/lib/__tests__/multichat-panel.spec.js +116 -0
  5. package/lib/components/chat.d.ts +6 -0
  6. package/lib/components/input/buttons/send-button.d.ts +1 -1
  7. package/lib/components/input/buttons/send-button.js +108 -8
  8. package/lib/components/input/chat-input.js +4 -3
  9. package/lib/components/messages/chat-body-placeholder.d.ts +6 -0
  10. package/lib/components/messages/chat-body-placeholder.js +19 -0
  11. package/lib/components/messages/index.d.ts +1 -0
  12. package/lib/components/messages/index.js +1 -0
  13. package/lib/components/messages/message-renderer.js +1 -1
  14. package/lib/components/messages/messages.js +54 -2
  15. package/lib/components/scroll-container.d.ts +6 -13
  16. package/lib/components/scroll-container.js +11 -22
  17. package/lib/input-model.d.ts +4 -0
  18. package/lib/theme-provider.js +2 -2
  19. package/lib/tokens.d.ts +50 -1
  20. package/lib/tokens.js +12 -0
  21. package/lib/types.d.ts +4 -0
  22. package/lib/widgets/chat-widget.js +5 -1
  23. package/lib/widgets/index.d.ts +1 -0
  24. package/lib/widgets/index.js +1 -0
  25. package/lib/widgets/multichat-panel.d.ts +15 -0
  26. package/lib/widgets/multichat-panel.js +41 -9
  27. package/lib/widgets/placeholder.d.ts +48 -0
  28. package/lib/widgets/placeholder.js +48 -0
  29. package/package.json +3 -3
  30. package/src/__tests__/chat-body-placeholder.spec.ts +69 -0
  31. package/src/__tests__/multichat-panel.spec.ts +135 -0
  32. package/src/components/chat.tsx +6 -0
  33. package/src/components/input/buttons/send-button.tsx +158 -14
  34. package/src/components/input/chat-input.tsx +6 -5
  35. package/src/components/messages/chat-body-placeholder.tsx +24 -0
  36. package/src/components/messages/index.ts +1 -0
  37. package/src/components/messages/message-renderer.tsx +1 -1
  38. package/src/components/messages/messages.tsx +59 -2
  39. package/src/components/scroll-container.tsx +27 -34
  40. package/src/input-model.ts +4 -0
  41. package/src/theme-provider.ts +2 -2
  42. package/src/tokens.ts +61 -1
  43. package/src/types.ts +4 -0
  44. package/src/widgets/chat-widget.tsx +14 -1
  45. package/src/widgets/index.ts +1 -0
  46. package/src/widgets/multichat-panel.tsx +56 -10
  47. package/src/widgets/placeholder.tsx +122 -0
  48. package/style/base.css +1 -0
  49. package/style/placeholder.css +29 -0
@@ -26,8 +26,9 @@ import { ISignal, Signal } from '@lumino/signaling';
26
26
  import { Widget } from '@lumino/widgets';
27
27
  import React, { useEffect, useRef, useState } from 'react';
28
28
 
29
- import { ChatWidget } from './chat-widget';
30
29
  import { ChatSelectorPopup } from './chat-selector-popup';
30
+ import { ChatWidget } from './chat-widget';
31
+ import { defaultPlaceholder, Placeholder } from './placeholder';
31
32
  import {
32
33
  Chat,
33
34
  IInputToolbarRegistry,
@@ -36,6 +37,7 @@ import {
36
37
  import { TRANSLATION_DOMAIN } from '../context';
37
38
  import { chatIcon, readIcon } from '../icons';
38
39
  import { IChatModel } from '../model';
40
+ import { IChatPlaceholderFactory } from '../tokens';
39
41
 
40
42
  const SIDEPANEL_CLASS = 'jp-chat-sidepanel';
41
43
  const ADD_BUTTON_CLASS = 'jp-chat-add';
@@ -69,6 +71,7 @@ export class MultiChatPanel extends PanelWithToolbar {
69
71
  this._createModel = options.createModel;
70
72
  this._openInMain = options.openInMain;
71
73
  this._renameChat = options.renameChat;
74
+ this._placeholderFactory = options.placeholderFactory;
72
75
 
73
76
  if (this._createModel) {
74
77
  // Add chat button calls the createChat callback
@@ -111,13 +114,18 @@ export class MultiChatPanel extends PanelWithToolbar {
111
114
  // Insert the toolbar as first child.
112
115
  this.insertWidget(0, this.toolbar);
113
116
  this._updateChatListDebouncer = new Debouncer(this._updateChatList, 200);
117
+
118
+ // Add the placeholder by default.
119
+ this._addPlaceholder();
114
120
  }
115
121
 
116
122
  /**
117
123
  * The currently displayed chat widget.
118
124
  */
119
125
  get current(): SidePanelWidget | undefined {
120
- return this._currentWidget;
126
+ return this._currentWidget instanceof SidePanelWidget
127
+ ? this._currentWidget
128
+ : undefined;
121
129
  }
122
130
 
123
131
  /**
@@ -178,15 +186,16 @@ export class MultiChatPanel extends PanelWithToolbar {
178
186
  const model = this._loadedModels.get(name);
179
187
  if (model) {
180
188
  // If this is the currently displayed chat, remove it.
181
- if (this._currentWidget?.model === model) {
182
- this._currentWidget.nameChanged.disconnect(this._modelNameChanged);
183
- this._currentWidget.dispose();
189
+ if (this.current?.model === model) {
190
+ this.current.nameChanged.disconnect(this._modelNameChanged);
191
+ this.current.dispose();
184
192
  this._currentWidget = undefined;
185
193
 
186
194
  // Clear current chat in selector
187
195
  if (this._chatSelectorPopup) {
188
196
  this._chatSelectorPopup.setCurrentChat(null);
189
197
  }
198
+ this._addPlaceholder();
190
199
  }
191
200
 
192
201
  model.dispose();
@@ -205,6 +214,29 @@ export class MultiChatPanel extends PanelWithToolbar {
205
214
  this._visibilityChanged.emit(false);
206
215
  }
207
216
 
217
+ /**
218
+ * Add a placeholder in the panel.
219
+ */
220
+ private _addPlaceholder(): void {
221
+ const props: Placeholder.IProps = {
222
+ chatNames: this._chatNames,
223
+ open: this._onSelectChat,
224
+ onCreate: this._createModel
225
+ ? async () => {
226
+ const args = await this._createModel!();
227
+ this.open(args);
228
+ }
229
+ : undefined,
230
+ chatNamesChanged: this._chatNamesChanged,
231
+ trans: this._trans
232
+ };
233
+ const placeholder = this._placeholderFactory
234
+ ? this._placeholderFactory.create(props)
235
+ : new defaultPlaceholder(props);
236
+ this.addWidget(placeholder);
237
+ this._currentWidget = placeholder;
238
+ }
239
+
208
240
  /**
209
241
  * Open a specific chat by name, creating a new sidepanel widget.
210
242
  */
@@ -214,9 +246,9 @@ export class MultiChatPanel extends PanelWithToolbar {
214
246
  return;
215
247
  }
216
248
 
217
- // Dispose current chat widget if any
249
+ // Dispose of the current chat widget or placeholder.
218
250
  if (this._currentWidget) {
219
- this._currentWidget.nameChanged.disconnect(this._modelNameChanged);
251
+ this.current?.nameChanged.disconnect(this._modelNameChanged);
220
252
  this._currentWidget.dispose();
221
253
  }
222
254
 
@@ -251,7 +283,7 @@ export class MultiChatPanel extends PanelWithToolbar {
251
283
  this.update();
252
284
  this._currentWidget = widget;
253
285
 
254
- this._currentWidget.nameChanged.connect(this._modelNameChanged);
286
+ this.current?.nameChanged.connect(this._modelNameChanged);
255
287
 
256
288
  // Update selector to show current chat
257
289
  if (this._chatSelectorPopup) {
@@ -283,6 +315,7 @@ export class MultiChatPanel extends PanelWithToolbar {
283
315
  ) {
284
316
  this._chatNames = chatNames ?? {};
285
317
  this._chatSelectorPopup?.updateChats(Object.keys(this._chatNames));
318
+ this._chatNamesChanged.emit(this._chatNames);
286
319
  }
287
320
  } catch (e) {
288
321
  console.error('Error getting chat files', e);
@@ -355,7 +388,7 @@ export class MultiChatPanel extends PanelWithToolbar {
355
388
  this._loadedModels.set(change.new, model);
356
389
  this._loadedModels.delete(change.old);
357
390
  this._chatSelectorPopup?.setLoadedModels(this.getLoadedModelNames());
358
- if (this._currentWidget?.model.name === model.name) {
391
+ if (this.current?.model.name === model.name) {
359
392
  this._chatSelectorPopup?.setCurrentChat(change.new);
360
393
  }
361
394
  }
@@ -382,6 +415,10 @@ export class MultiChatPanel extends PanelWithToolbar {
382
415
  };
383
416
 
384
417
  private _chatOpened = new Signal<MultiChatPanel, ChatWidget>(this);
418
+ private _chatNamesChanged = new Signal<
419
+ MultiChatPanel,
420
+ { [name: string]: string }
421
+ >(this);
385
422
  private _chatOptions: Omit<Chat.IOptions, 'model' | 'inputToolbarRegistry'>;
386
423
  private _inputToolbarFactory?: IInputToolbarRegistryFactory;
387
424
  private _updateChatListDebouncer: Debouncer;
@@ -392,10 +429,11 @@ export class MultiChatPanel extends PanelWithToolbar {
392
429
  private _getChatNames?: () => Promise<{ [name: string]: string }>;
393
430
  private _openInMain?: (name: string) => Promise<boolean>;
394
431
  private _renameChat?: boolean | ((oldName: string) => Promise<string | null>);
432
+ private _placeholderFactory?: IChatPlaceholderFactory;
395
433
  private _openChatWidget?: ReactWidget;
396
434
  private _chatSelectorPopup?: ChatSelectorPopup;
397
435
  private _loadedModels: Map<string, IChatModel> = new Map();
398
- private _currentWidget?: SidePanelWidget;
436
+ private _currentWidget?: SidePanelWidget | Widget;
399
437
  private _chatNames: { [name: string]: string } = {};
400
438
  private _visibilityChanged = new Signal<MultiChatPanel, boolean>(this);
401
439
  private _trans: TranslationBundle;
@@ -442,6 +480,14 @@ export namespace MultiChatPanel {
442
480
  * @returns - a boolean, whether the chat has been renamed or not.
443
481
  */
444
482
  renameChat?: boolean | ((oldName: string) => Promise<string | null>);
483
+ /**
484
+ * An optional factory to create a placeholder widget displayed when no chat
485
+ * is opened. Falls back to the default placeholder if not provided.
486
+ *
487
+ * @param props - the props passed to the placeholder.
488
+ * @returns a widget to display as placeholder.
489
+ */
490
+ placeholderFactory?: IChatPlaceholderFactory;
445
491
  }
446
492
  /**
447
493
  * The options for the add chat method.
@@ -0,0 +1,122 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import { IRenderMime } from '@jupyterlab/rendermime';
7
+ import {
8
+ addIcon,
9
+ ReactWidget,
10
+ ToolbarButtonComponent
11
+ } from '@jupyterlab/ui-components';
12
+ import { ISignal } from '@lumino/signaling';
13
+ import React, { useEffect, useState } from 'react';
14
+
15
+ import { useTranslator } from '../context';
16
+
17
+ /**
18
+ * The default placeholder widget.
19
+ */
20
+ export class defaultPlaceholder extends ReactWidget {
21
+ constructor(options: Placeholder.IProps) {
22
+ super();
23
+ this._props = options;
24
+ }
25
+
26
+ render() {
27
+ return <PlaceholderComponent {...this._props} />;
28
+ }
29
+
30
+ private _props: Placeholder.IProps;
31
+ }
32
+
33
+ /**
34
+ * The placeholder namespace.
35
+ */
36
+ export namespace Placeholder {
37
+ /**
38
+ * The options of the placeholder widget.
39
+ */
40
+ export interface IProps {
41
+ /**
42
+ * The initial chat names.
43
+ */
44
+ chatNames: { [name: string]: string };
45
+ /**
46
+ * A callback to open an existing chat by name.
47
+ *
48
+ * @param name - the display name of the chat to open.
49
+ */
50
+ open: (name: string) => Promise<void>;
51
+ /**
52
+ * A callback to create and open a new chat.
53
+ */
54
+ onCreate?: () => Promise<void>;
55
+ /**
56
+ * An optional signal emitting when the chat list changes.
57
+ */
58
+ chatNamesChanged?: ISignal<any, { [name: string]: string }>;
59
+ /**
60
+ * An optional translation bundle.
61
+ */
62
+ trans?: IRenderMime.TranslationBundle;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * The default placeholder component.
68
+ */
69
+ const PlaceholderComponent = (props: Placeholder.IProps): JSX.Element => {
70
+ const trans = props.trans ?? useTranslator();
71
+
72
+ const [chatNames, setChatNames] = useState<{ [name: string]: string }>(
73
+ props.chatNames
74
+ );
75
+
76
+ useEffect(() => {
77
+ if (!props.chatNamesChanged) {
78
+ return;
79
+ }
80
+ const onChanged = (_: any, names: { [name: string]: string }) => {
81
+ setChatNames(names);
82
+ };
83
+ props.chatNamesChanged.connect(onChanged);
84
+ return () => {
85
+ props.chatNamesChanged!.disconnect(onChanged);
86
+ };
87
+ }, [props.chatNamesChanged]);
88
+
89
+ const names = Object.keys(chatNames).sort((a, b) => a.localeCompare(b));
90
+
91
+ return (
92
+ <div className="jp-chat-placeholder">
93
+ <h3>{trans.__('No chat opened')}</h3>
94
+ {props.onCreate && (
95
+ <div className="jp-chat-placeholder-hint">
96
+ <ToolbarButtonComponent
97
+ label={trans.__('New chat')}
98
+ icon={addIcon}
99
+ onClick={props.onCreate}
100
+ tooltip={trans.__('Create a new chat')}
101
+ className="jp-chat-add"
102
+ />
103
+ </div>
104
+ )}
105
+ {names.length > 0 && (
106
+ <div className="jp-chat-placeholder-list">
107
+ <p>{trans.__('Open an existing chat:')}</p>
108
+ {names.map(name => (
109
+ <div key={name}>
110
+ <button
111
+ className="jp-chat-placeholder-chat-item"
112
+ onClick={() => props.open(name)}
113
+ >
114
+ {name}
115
+ </button>
116
+ </div>
117
+ ))}
118
+ </div>
119
+ )}
120
+ </div>
121
+ );
122
+ };
package/style/base.css CHANGED
@@ -13,3 +13,4 @@
13
13
  @import url('./chat-settings.css');
14
14
  @import url('./chat-selector.css');
15
15
  @import url('./input.css');
16
+ @import url('./placeholder.css');
@@ -0,0 +1,29 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ .jp-chat-placeholder {
7
+ text-align: center;
8
+ color: var(--jp-content-font-color2);
9
+ }
10
+
11
+ .jp-chat-placeholder > h3 {
12
+ margin-bottom: var(--jp-content-heading-margin-bottom);
13
+ }
14
+
15
+ .jp-chat-placeholder-chat-item {
16
+ color: var(--jp-content-font-color1);
17
+ background: none;
18
+ border: none;
19
+ padding: 2px 0;
20
+ font: inherit;
21
+ cursor: pointer;
22
+ text-align: left;
23
+ text-decoration: none;
24
+ }
25
+
26
+ .jp-chat-placeholder-chat-item:hover {
27
+ text-decoration: underline;
28
+ font-weight: bold;
29
+ }